diff --git a/autoarray/__init__.py b/autoarray/__init__.py index 4a81a3b2a..e2595203f 100644 --- a/autoarray/__init__.py +++ b/autoarray/__init__.py @@ -23,25 +23,21 @@ from .fit.fit_imaging import FitImaging from .fit.fit_interferometer import FitInterferometer from .geometry.geometry_2d import Geometry2D -from .inversion.pixelization.mappers.abstract import AbstractMapper -from .inversion.pixelization import mesh -from .inversion.pixelization import image_mesh +from .inversion.mesh import mesh +from .inversion.mesh import image_mesh from .inversion import regularization as reg -from .inversion.inversion.settings import SettingsInversion +from .settings import Settings from .inversion.inversion.abstract import AbstractInversion from .inversion.regularization.abstract import AbstractRegularization from .inversion.inversion.factory import inversion_from as Inversion from .inversion.inversion.dataset_interface import DatasetInterface -from .inversion.pixelization.border_relocator import BorderRelocator -from .inversion.pixelization.pixelization import Pixelization -from .inversion.pixelization.mappers.abstract import AbstractMapper -from .inversion.pixelization.mappers.mapper_grids import MapperGrids -from .inversion.pixelization.mappers.factory import mapper_from as Mapper -from .inversion.pixelization.mappers.rectangular import MapperRectangular -from .inversion.pixelization.mappers.delaunay import MapperDelaunay -from .inversion.pixelization.mappers.rectangular_uniform import MapperRectangularUniform -from .inversion.pixelization.image_mesh.abstract import AbstractImageMesh -from .inversion.pixelization.mesh.abstract import AbstractMesh +from .inversion.mesh.border_relocator import BorderRelocator +from .inversion.pixelization import Pixelization +from .inversion.mappers.abstract import Mapper +from .inversion.mesh.image_mesh.abstract import AbstractImageMesh +from .inversion.mesh.mesh.abstract import AbstractMesh +from .inversion.mesh.interpolator.rectangular import InterpolatorRectangular +from .inversion.mesh.interpolator.delaunay import InterpolatorDelaunay from .inversion.inversion.imaging.mapping import InversionImagingMapping from .inversion.inversion.imaging.sparse import InversionImagingSparse from .inversion.inversion.imaging.inversion_imaging_util import ImagingSparseOperator @@ -77,9 +73,10 @@ from .structures.grids.uniform_2d import Grid2D from .operators.over_sampling.over_sampler import OverSampler from .structures.grids.irregular_2d import Grid2DIrregular -from .structures.mesh.rectangular_2d import Mesh2DRectangular -from .structures.mesh.rectangular_2d_uniform import Mesh2DRectangularUniform -from .structures.mesh.delaunay_2d import Mesh2DDelaunay +from .inversion.mesh.mesh_geometry.rectangular import MeshGeometryRectangular +from .inversion.mesh.mesh_geometry.delaunay import MeshGeometryDelaunay +from .inversion.mesh.interpolator.rectangular import InterpolatorRectangular +from .inversion.mesh.interpolator.delaunay import InterpolatorDelaunay from .structures.arrays.kernel_2d import Kernel2D from .structures.vectors.uniform import VectorYX2D from .structures.vectors.irregular import VectorYX2DIrregular diff --git a/autoarray/config/general.yaml b/autoarray/config/general.yaml index c7c045969..257cae846 100644 --- a/autoarray/config/general.yaml +++ b/autoarray/config/general.yaml @@ -6,7 +6,6 @@ inversion: check_reconstruction: true # If True, the inversion's reconstruction is checked to ensure the solution of a meshs's mapper is not an invalid solution where the values are all the same. use_positive_only_solver: true # If True, inversion's use a positive-only linear algebra solver by default, which is slower but prevents unphysical negative values in the reconstructed solutuion. no_regularization_add_to_curvature_diag_value : 1.0e-3 # The default value added to the curvature matrix's diagonal when regularization is not applied to a linear object, which prevents inversion's failing due to the matrix being singular. - positive_only_uses_p_initial: true # If True, the positive-only solver of an inversion's uses an initial guess of the reconstructed data's values as which values should be positive, speeding up the solver. use_border_relocator: false # If True, by default a pixelization's border is used to relocate all pixels outside its border to the border. reconstruction_vmax_factor: 0.5 # Plots of an Inversion's reconstruction use the reconstructed data's bright value multiplied by this factor. numba: diff --git a/autoarray/dataset/grids.py b/autoarray/dataset/grids.py index 60cf8baea..8aa6626fa 100644 --- a/autoarray/dataset/grids.py +++ b/autoarray/dataset/grids.py @@ -5,7 +5,7 @@ from autoarray.structures.arrays.kernel_2d import Kernel2D from autoarray.structures.grids.uniform_2d import Grid2D -from autoarray.inversion.pixelization.border_relocator import BorderRelocator +from autoarray.inversion.mesh.border_relocator import BorderRelocator from autoarray import exc diff --git a/autoarray/fixtures.py b/autoarray/fixtures.py index 2b419b185..d07b981fe 100644 --- a/autoarray/fixtures.py +++ b/autoarray/fixtures.py @@ -332,13 +332,11 @@ def make_regularization_constant_split(): def make_regularization_adaptive_brightness(): - return aa.reg.AdaptiveBrightness( - inner_coefficient=0.1, outer_coefficient=10.0, signal_scale=0.5 - ) + return aa.reg.Adapt(inner_coefficient=0.1, outer_coefficient=10.0, signal_scale=0.5) def make_regularization_adaptive_brightness_split(): - return aa.reg.AdaptiveBrightnessSplit( + return aa.reg.AdaptSplit( inner_coefficient=0.1, outer_coefficient=10.0, signal_scale=0.5 ) @@ -355,13 +353,44 @@ def make_regularization_matern_kernel(): return aa.reg.MaternKernel(coefficient=1.0, scale=0.5, nu=0.7) -def make_rectangular_mesh_grid_3x3(): - return aa.Mesh2DRectangular.overlay_grid( - grid=make_grid_2d_sub_2_7x7().over_sampled, shape_native=(3, 3) +def make_over_sampler_2d_7x7(): + return aa.OverSampler(mask=make_mask_2d_7x7(), sub_size=2) + + +def make_border_relocator_2d_7x7(): + return aa.BorderRelocator( + mask=make_mask_2d_7x7(), sub_size=np.array([2, 2, 2, 2, 2, 2, 2, 2, 2]) + ) + + +def make_rectangular_mapper_7x7_3x3(): + + from autoarray.inversion.mesh.mesh.rectangular_adapt_density import ( + overlay_grid_from, ) + shape_native = (3, 3) + + source_plane_mesh_grid = overlay_grid_from( + shape_native=shape_native, grid=make_grid_2d_sub_2_7x7().over_sampled + ) + + mesh = aa.mesh.RectangularUniform(shape=shape_native) + + interpolator = mesh.interpolator_from( + source_plane_data_grid=make_grid_2d_sub_2_7x7(), + source_plane_mesh_grid=aa.Grid2DIrregular(source_plane_mesh_grid), + adapt_data=aa.Array2D.ones(shape_native, pixel_scales=0.1), + ) + + return aa.Mapper( + interpolator=interpolator, + regularization=make_regularization_constant(), + ) + + +def make_delaunay_mapper_9_3x3(): -def make_delaunay_mesh_grid_9(): grid_9 = aa.Grid2D.no_mask( values=[ [0.6, -0.3], @@ -378,51 +407,51 @@ def make_delaunay_mesh_grid_9(): pixel_scales=1.0, ) - return aa.Mesh2DDelaunay( - values=grid_9, - source_plane_data_grid_over_sampled=make_grid_2d_sub_2_7x7().over_sampled, - ) - - -def make_over_sampler_2d_7x7(): - return aa.OverSampler(mask=make_mask_2d_7x7(), sub_size=2) + mesh = aa.mesh.Delaunay() - -def make_border_relocator_2d_7x7(): - return aa.BorderRelocator( - mask=make_mask_2d_7x7(), sub_size=np.array([2, 2, 2, 2, 2, 2, 2, 2, 2]) - ) - - -def make_rectangular_mapper_7x7_3x3(): - mapper_grids = aa.MapperGrids( - mask=make_mask_2d_7x7(), + interpolator = mesh.interpolator_from( source_plane_data_grid=make_grid_2d_sub_2_7x7(), - source_plane_mesh_grid=make_rectangular_mesh_grid_3x3(), - image_plane_mesh_grid=None, + source_plane_mesh_grid=grid_9, adapt_data=aa.Array2D.ones(shape_native=(3, 3), pixel_scales=0.1), ) - return aa.MapperRectangularUniform( - mapper_grids=mapper_grids, - border_relocator=make_border_relocator_2d_7x7(), + return aa.Mapper( + interpolator=interpolator, regularization=make_regularization_constant(), + image_plane_mesh_grid=aa.Grid2D.uniform(shape_native=(3, 3), pixel_scales=0.1), ) -def make_delaunay_mapper_9_3x3(): - mapper_grids = aa.MapperGrids( - mask=make_mask_2d_7x7(), +def make_knn_mapper_9_3x3(): + + grid_9 = aa.Grid2D.no_mask( + values=[ + [0.6, -0.3], + [0.5, -0.8], + [0.2, 0.1], + [0.0, 0.5], + [-0.3, -0.8], + [-0.6, -0.5], + [-0.4, -1.1], + [-1.2, 0.8], + [-1.5, 0.9], + ], + shape_native=(3, 3), + pixel_scales=1.0, + ) + + mesh = aa.mesh.KNearestNeighbor(split_neighbor_division=1) + + interpolator = mesh.interpolator_from( source_plane_data_grid=make_grid_2d_sub_2_7x7(), - source_plane_mesh_grid=make_delaunay_mesh_grid_9(), - image_plane_mesh_grid=aa.Grid2D.uniform(shape_native=(3, 3), pixel_scales=0.1), + source_plane_mesh_grid=grid_9, adapt_data=aa.Array2D.ones(shape_native=(3, 3), pixel_scales=0.1), ) - return aa.MapperDelaunay( - mapper_grids=mapper_grids, - border_relocator=make_border_relocator_2d_7x7(), + return aa.Mapper( + interpolator=interpolator, regularization=make_regularization_constant(), + image_plane_mesh_grid=aa.Grid2D.uniform(shape_native=(3, 3), pixel_scales=0.1), ) diff --git a/autoarray/inversion/inversion/abstract.py b/autoarray/inversion/inversion/abstract.py index ae991a494..af5cd189e 100644 --- a/autoarray/inversion/inversion/abstract.py +++ b/autoarray/inversion/inversion/abstract.py @@ -9,9 +9,9 @@ from autoarray.dataset.interferometer.dataset import Interferometer from autoarray.inversion.inversion.dataset_interface import DatasetInterface from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper +from autoarray.inversion.mappers.abstract import Mapper from autoarray.inversion.regularization.abstract import AbstractRegularization -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.preloads import Preloads from autoarray.structures.arrays.uniform_2d import Array2D from autoarray.structures.grids.irregular_2d import Grid2DIrregular @@ -26,7 +26,7 @@ def __init__( self, dataset: Union[Imaging, Interferometer, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, preloads: Preloads = None, xp=np, ): @@ -72,7 +72,7 @@ def __init__( self.linear_obj_list = linear_obj_list - self.settings = settings + self.settings = settings or Settings() self.preloads = preloads or Preloads() @@ -143,7 +143,7 @@ def param_range_list_from(self, cls: Type) -> List[List[int]]: - The first `Mapper` values are in the entries `[3:103]`. - The second `Mapper` values are in the entries `[103:303] - For this example, `param_range_list_from(cls=AbstractMapper)` therefore returns the + For this example, `param_range_list_from(cls=Mapper)` therefore returns the list `[[3, 103], [103, 303]]`. Parameters @@ -239,7 +239,7 @@ def mapper_indices(self) -> np.ndarray: mapper_indices = [] - param_range_list = self.param_range_list_from(cls=AbstractMapper) + param_range_list = self.param_range_list_from(cls=Mapper) for param_range in param_range_list: @@ -713,7 +713,7 @@ def regularization_weights_from(self, index: int) -> np.ndarray: def regularization_weights_mapper_dict(self) -> Dict[LinearObj, np.ndarray]: regularization_weights_dict = {} - for index, mapper in enumerate(self.cls_list_from(cls=AbstractMapper)): + for index, mapper in enumerate(self.cls_list_from(cls=Mapper)): regularization_weights_dict[mapper] = self.regularization_weights_from( index=index, ) @@ -773,7 +773,7 @@ def max_pixel_list_from( ------- """ - mapper = self.cls_list_from(cls=AbstractMapper)[mapper_index] + mapper = self.cls_list_from(cls=Mapper)[mapper_index] reconstruction = self.reconstruction_dict[mapper] max_pixel_list = [] @@ -818,7 +818,7 @@ def max_pixel_centre(self, mapper_index: int = 0) -> Grid2DIrregular: ------- The centre of the brightest pixel in the mapper values. """ - mapper = self.cls_list_from(cls=AbstractMapper)[mapper_index] + mapper = self.cls_list_from(cls=Mapper)[mapper_index] reconstruction = self.reconstruction_dict[mapper] max_pixel = np.argmax(reconstruction) diff --git a/autoarray/inversion/inversion/factory.py b/autoarray/inversion/inversion/factory.py index b1e9edaf8..e9955127a 100644 --- a/autoarray/inversion/inversion/factory.py +++ b/autoarray/inversion/inversion/factory.py @@ -23,7 +23,7 @@ from autoarray.inversion.inversion.imaging.sparse import ( InversionImagingSparse, ) -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.preloads import Preloads from autoarray.structures.arrays.uniform_2d import Array2D @@ -31,7 +31,7 @@ def inversion_from( dataset: Union[Imaging, Interferometer, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, preloads: Preloads = None, xp=np, ): @@ -80,7 +80,7 @@ def inversion_from( def inversion_imaging_from( dataset, linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, preloads: Preloads = None, xp=np, ): @@ -157,7 +157,7 @@ def inversion_imaging_from( def inversion_interferometer_from( dataset: Union[Interferometer, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, xp=np, ): """ diff --git a/autoarray/inversion/inversion/imaging/abstract.py b/autoarray/inversion/inversion/imaging/abstract.py index 4269306b1..85230ccf6 100644 --- a/autoarray/inversion/inversion/imaging/abstract.py +++ b/autoarray/inversion/inversion/imaging/abstract.py @@ -4,10 +4,10 @@ from autoarray.dataset.imaging.dataset import Imaging from autoarray.inversion.inversion.dataset_interface import DatasetInterface from autoarray.inversion.linear_obj.func_list import AbstractLinearObjFuncList -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper +from autoarray.inversion.mappers.abstract import Mapper from autoarray.inversion.inversion.abstract import AbstractInversion from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.preloads import Preloads from autoarray.inversion.inversion.imaging import inversion_imaging_util @@ -18,7 +18,7 @@ def __init__( self, dataset: Union[Imaging, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, preloads: Preloads = None, xp=np, ): @@ -255,7 +255,7 @@ def mapper_operated_mapping_matrix_dict(self) -> Dict: """ mapper_operated_mapping_matrix_dict = {} - for mapper in self.cls_list_from(cls=AbstractMapper): + for mapper in self.cls_list_from(cls=Mapper): operated_mapping_matrix = self.psf.convolved_mapping_matrix_from( mapping_matrix=mapper.mapping_matrix, mask=self.mask, xp=self._xp ) diff --git a/autoarray/inversion/inversion/imaging/mapping.py b/autoarray/inversion/inversion/imaging/mapping.py index 318a55800..39ac0501e 100644 --- a/autoarray/inversion/inversion/imaging/mapping.py +++ b/autoarray/inversion/inversion/imaging/mapping.py @@ -7,8 +7,8 @@ from autoarray.inversion.inversion.dataset_interface import DatasetInterface from autoarray.inversion.inversion.imaging.abstract import AbstractInversionImaging from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.inversion.mappers.abstract import Mapper +from autoarray.settings import Settings from autoarray.preloads import Preloads from autoarray.structures.arrays.uniform_2d import Array2D @@ -21,7 +21,7 @@ def __init__( self, dataset: Union[Imaging, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, preloads: Preloads = None, xp=np, ): diff --git a/autoarray/inversion/inversion/imaging/sparse.py b/autoarray/inversion/inversion/imaging/sparse.py index 8f6ab1ba5..ba32eb850 100644 --- a/autoarray/inversion/inversion/imaging/sparse.py +++ b/autoarray/inversion/inversion/imaging/sparse.py @@ -7,9 +7,9 @@ from autoarray.inversion.inversion.dataset_interface import DatasetInterface from autoarray.inversion.inversion.imaging.abstract import AbstractInversionImaging from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.inversion.linear_obj.func_list import AbstractLinearObjFuncList -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper +from autoarray.inversion.mappers.abstract import Mapper from autoarray.preloads import Preloads from autoarray.structures.arrays.uniform_2d import Array2D @@ -21,7 +21,7 @@ def __init__( self, dataset: Union[Imaging, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, preloads: Preloads = None, xp=np, ): @@ -72,13 +72,13 @@ def _data_vector_mapper(self) -> np.ndarray: in the inversion, and is separated into a separate method to enable preloading of the mapper `data_vector`. """ - if not self.has(cls=AbstractMapper): + if not self.has(cls=Mapper): return None data_vector = self._xp.zeros(self.total_params) - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range = self.param_range_list_from(cls=Mapper) for mapper_index, mapper in enumerate(mapper_list): @@ -121,7 +121,7 @@ def data_vector(self) -> np.ndarray: """ if self.has(cls=AbstractLinearObjFuncList): return self._data_vector_func_list_and_mapper - elif self.total(cls=AbstractMapper) == 1: + elif self.total(cls=Mapper) == 1: return self._data_vector_x1_mapper return self._data_vector_multi_mapper @@ -158,7 +158,7 @@ def _data_vector_multi_mapper(self) -> np.ndarray: data_vector_list = [] - for mapper in self.cls_list_from(cls=AbstractMapper): + for mapper in self.cls_list_from(cls=Mapper): rows, cols, vals = mapper.sparse_triplets_data @@ -243,7 +243,7 @@ def curvature_matrix(self) -> np.ndarray: """ if self.has(cls=AbstractLinearObjFuncList): curvature_matrix = self._curvature_matrix_func_list_and_mapper - elif self.total(cls=AbstractMapper) == 1: + elif self.total(cls=Mapper) == 1: curvature_matrix = self._curvature_matrix_x1_mapper else: curvature_matrix = self._curvature_matrix_multi_mapper @@ -276,13 +276,13 @@ def _curvature_matrix_mapper_diag(self) -> Optional[np.ndarray]: other calculations to enable preloading of this calculation. """ - if not self.has(cls=AbstractMapper): + if not self.has(cls=Mapper): return None curvature_matrix = self._xp.zeros((self.total_params, self.total_params)) - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range_list = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range_list = self.param_range_list_from(cls=Mapper) for i in range(len(mapper_list)): mapper_i = mapper_list[i] @@ -304,13 +304,13 @@ def _curvature_matrix_mapper_diag(self) -> Optional[np.ndarray]: else: curvature_matrix = curvature_matrix.at[start:end, start:end].set(diag) - if self.total(cls=AbstractMapper) == 1: + if self.total(cls=Mapper) == 1: return curvature_matrix return curvature_matrix def _curvature_matrix_off_diag_from( - self, mapper_0: AbstractMapper, mapper_1: AbstractMapper + self, mapper_0: Mapper, mapper_1: Mapper ) -> np.ndarray: """ The `curvature_matrix` is a 2D matrix which uses the mappings between the data and the linear objects to @@ -362,11 +362,11 @@ def _curvature_matrix_multi_mapper(self) -> np.ndarray: curvature_matrix = self._curvature_matrix_mapper_diag - if self.total(cls=AbstractMapper) == 1: + if self.total(cls=Mapper) == 1: return curvature_matrix - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range_list = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range_list = self.param_range_list_from(cls=Mapper) for i in range(len(mapper_list)): mapper_i = mapper_list[i] @@ -401,8 +401,8 @@ def _curvature_matrix_func_list_and_mapper(self) -> np.ndarray: curvature_matrix = self._curvature_matrix_multi_mapper - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range_list = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range_list = self.param_range_list_from(cls=Mapper) linear_func_list = self.cls_list_from(cls=AbstractLinearObjFuncList) linear_func_param_range_list = self.param_range_list_from( @@ -491,7 +491,7 @@ def _mapped_reconstructed_data_dict_from( """ Shared implementation for mapping a reconstruction to image-plane arrays for each linear object. - For AbstractMapper objects this uses the sparse operator mapping, and optionally applies the PSF. + For Mapper objects this uses the sparse operator mapping, and optionally applies the PSF. For linear-func objects this uses either the operated or unoperated mapping matrix dict. """ mapped_dict = {} @@ -504,7 +504,7 @@ def _mapped_reconstructed_data_dict_from( reconstruction = reconstruction_dict[linear_obj] - if isinstance(linear_obj, AbstractMapper): + if isinstance(linear_obj, Mapper): rows, cols, vals = linear_obj.sparse_triplets_curvature diff --git a/autoarray/inversion/inversion/imaging_numba/sparse.py b/autoarray/inversion/inversion/imaging_numba/sparse.py index 91bb74577..ed22dc135 100644 --- a/autoarray/inversion/inversion/imaging_numba/sparse.py +++ b/autoarray/inversion/inversion/imaging_numba/sparse.py @@ -7,9 +7,9 @@ from autoarray.inversion.inversion.dataset_interface import DatasetInterface from autoarray.inversion.inversion.imaging.abstract import AbstractInversionImaging from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.inversion.linear_obj.func_list import AbstractLinearObjFuncList -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper +from autoarray.inversion.mappers.abstract import Mapper from autoarray.preloads import Preloads from autoarray.structures.arrays.uniform_2d import Array2D @@ -21,7 +21,7 @@ def __init__( self, dataset: Union[Imaging, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, preloads: Preloads = None, xp=np, ): @@ -76,13 +76,13 @@ def _data_vector_mapper(self) -> np.ndarray: in the inversion, and is separated into a separate method to enable preloading of the mapper `data_vector`. """ - if not self.has(cls=AbstractMapper): + if not self.has(cls=Mapper): return None data_vector = np.zeros(self.total_params) - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range = self.param_range_list_from(cls=Mapper) for mapper_index, mapper in enumerate(mapper_list): @@ -119,7 +119,7 @@ def data_vector(self) -> np.ndarray: """ if self.has(cls=AbstractLinearObjFuncList): return self._data_vector_func_list_and_mapper - elif self.total(cls=AbstractMapper) == 1: + elif self.total(cls=Mapper) == 1: return self._data_vector_x1_mapper return self._data_vector_multi_mapper @@ -154,7 +154,7 @@ def _data_vector_multi_mapper(self) -> np.ndarray: data_vector_list = [] - for mapper in self.cls_list_from(cls=AbstractMapper): + for mapper in self.cls_list_from(cls=Mapper): rows, cols, vals = mapper.sparse_triplets_data @@ -233,7 +233,7 @@ def curvature_matrix(self) -> np.ndarray: """ if self.has(cls=AbstractLinearObjFuncList): curvature_matrix = self._curvature_matrix_func_list_and_mapper - elif self.total(cls=AbstractMapper) == 1: + elif self.total(cls=Mapper) == 1: curvature_matrix = self._curvature_matrix_x1_mapper else: curvature_matrix = self._curvature_matrix_multi_mapper @@ -264,13 +264,13 @@ def _curvature_matrix_mapper_diag(self) -> Optional[np.ndarray]: other calculations to enable preloading of this calculation. """ - if not self.has(cls=AbstractMapper): + if not self.has(cls=Mapper): return None curvature_matrix = np.zeros((self.total_params, self.total_params)) - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range_list = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range_list = self.param_range_list_from(cls=Mapper) for i in range(len(mapper_list)): mapper_i = mapper_list[i] @@ -293,13 +293,13 @@ def _curvature_matrix_mapper_diag(self) -> Optional[np.ndarray]: mapper_param_range_i[0] : mapper_param_range_i[1], ] = diag - if self.total(cls=AbstractMapper) == 1: + if self.total(cls=Mapper) == 1: return curvature_matrix return curvature_matrix def _curvature_matrix_off_diag_from( - self, mapper_0: AbstractMapper, mapper_1: AbstractMapper + self, mapper_0: Mapper, mapper_1: Mapper ) -> np.ndarray: """ The `curvature_matrix` is a 2D matrix which uses the mappings between the data and the linear objects to @@ -364,11 +364,11 @@ def _curvature_matrix_multi_mapper(self) -> np.ndarray: curvature_matrix = self._curvature_matrix_mapper_diag - if self.total(cls=AbstractMapper) == 1: + if self.total(cls=Mapper) == 1: return curvature_matrix - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range_list = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range_list = self.param_range_list_from(cls=Mapper) for i in range(len(mapper_list)): mapper_i = mapper_list[i] @@ -403,8 +403,8 @@ def _curvature_matrix_func_list_and_mapper(self) -> np.ndarray: curvature_matrix = self._curvature_matrix_multi_mapper - mapper_list = self.cls_list_from(cls=AbstractMapper) - mapper_param_range_list = self.param_range_list_from(cls=AbstractMapper) + mapper_list = self.cls_list_from(cls=Mapper) + mapper_param_range_list = self.param_range_list_from(cls=Mapper) linear_func_list = self.cls_list_from(cls=AbstractLinearObjFuncList) linear_func_param_range_list = self.param_range_list_from( @@ -475,7 +475,7 @@ def _mapped_reconstructed_data_dict_from( """ Shared implementation for mapping a reconstruction to image-plane arrays for each linear object. - - AbstractMapper: uses unique mappings (w-tilde compatible) + PSF convolution. + - Mapper: uses unique mappings (w-tilde compatible) + PSF convolution. - Linear-func: uses either operated or unoperated mapping matrix dict. """ mapped_dict: Dict["LinearObj", "Array2D"] = {} @@ -487,7 +487,7 @@ def _mapped_reconstructed_data_dict_from( for linear_obj in self.linear_obj_list: reconstruction = reconstruction_dict[linear_obj] - if isinstance(linear_obj, AbstractMapper): + if isinstance(linear_obj, Mapper): mapped = inversion_imaging_numba_util.mapped_reconstructed_data_via_image_to_pix_unique_from( data_to_pix_unique=linear_obj.unique_mappings.data_to_pix_unique, diff --git a/autoarray/inversion/inversion/interferometer/abstract.py b/autoarray/inversion/inversion/interferometer/abstract.py index 958fa71c5..7b37f2c1f 100644 --- a/autoarray/inversion/inversion/interferometer/abstract.py +++ b/autoarray/inversion/inversion/interferometer/abstract.py @@ -6,7 +6,7 @@ from autoarray.inversion.inversion.abstract import AbstractInversion from autoarray.mask.mask_2d import Mask2D from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.structures.arrays.uniform_2d import Array2D from autoarray.inversion.inversion import inversion_util @@ -17,7 +17,7 @@ def __init__( self, dataset: Union[Interferometer, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, xp=np, ): """ diff --git a/autoarray/inversion/inversion/interferometer/mapping.py b/autoarray/inversion/inversion/interferometer/mapping.py index 223bc3387..8dfdea7d1 100644 --- a/autoarray/inversion/inversion/interferometer/mapping.py +++ b/autoarray/inversion/inversion/interferometer/mapping.py @@ -7,7 +7,7 @@ AbstractInversionInterferometer, ) from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.structures.visibilities import Visibilities from autoarray.inversion.inversion.interferometer import inversion_interferometer_util @@ -19,7 +19,7 @@ def __init__( self, dataset: Union[Interferometer, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, xp=np, ): """ diff --git a/autoarray/inversion/inversion/interferometer/sparse.py b/autoarray/inversion/inversion/interferometer/sparse.py index 28b9076fa..b5faa8afb 100644 --- a/autoarray/inversion/inversion/interferometer/sparse.py +++ b/autoarray/inversion/inversion/interferometer/sparse.py @@ -7,8 +7,8 @@ AbstractInversionInterferometer, ) from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.inversion.settings import SettingsInversion -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper +from autoarray.settings import Settings +from autoarray.inversion.mappers.abstract import Mapper from autoarray.structures.visibilities import Visibilities from autoarray.inversion.inversion.interferometer import inversion_interferometer_util @@ -19,7 +19,7 @@ def __init__( self, dataset: Union[Interferometer, DatasetInterface], linear_obj_list: List[LinearObj], - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, xp=np, ): """ @@ -99,7 +99,7 @@ def curvature_matrix_diag(self) -> np.ndarray: This function computes the diagonal terms of F using the sparse linear algebra formalism. """ - mapper = self.cls_list_from(cls=AbstractMapper)[0] + mapper = self.cls_list_from(cls=Mapper)[0] return self.dataset.sparse_operator.curvature_matrix_via_sparse_operator_from( pix_indexes_for_sub_slim_index=mapper.pix_indexes_for_sub_slim_index, diff --git a/autoarray/inversion/inversion/inversion_util.py b/autoarray/inversion/inversion/inversion_util.py index 7d2e6400d..a44311992 100644 --- a/autoarray/inversion/inversion/inversion_util.py +++ b/autoarray/inversion/inversion/inversion_util.py @@ -2,7 +2,7 @@ from typing import List, Optional, Type -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray import exc from autoarray.util.fnnls import fnnls_cholesky @@ -82,7 +82,7 @@ def curvature_matrix_via_mapping_matrix_from( noise_map: "np.ndarray", add_to_curvature_diag: bool = False, no_regularization_index_list: Optional[List] = None, - settings: "SettingsInversion" = SettingsInversion(), + settings: "Settings" = Settings(), xp=np, ) -> np.ndarray: """ @@ -218,7 +218,7 @@ def reconstruction_positive_negative_from( def reconstruction_positive_only_from( data_vector: np.ndarray, curvature_reg_matrix: np.ndarray, - settings: SettingsInversion = SettingsInversion(), + settings: Settings = None, xp=np, ): """ @@ -270,15 +270,11 @@ def reconstruction_positive_only_from( return jaxnnls.solve_nnls_primal(curvature_reg_matrix, data_vector) try: - if settings.positive_only_uses_p_initial: - P_initial = np.linalg.solve(curvature_reg_matrix, data_vector) > 0 - else: - P_initial = np.zeros(0, dtype=int) return fnnls_cholesky( curvature_reg_matrix, (data_vector).T, - P_initial=P_initial, + P_initial=np.linalg.solve(curvature_reg_matrix, data_vector) > 0, ) except (RuntimeError, np.linalg.LinAlgError, ValueError) as e: @@ -334,7 +330,7 @@ def param_range_list_from(cls: Type, linear_obj_list) -> List[List[int]]: - The first `Mapper` values are in the entries `[3:103]`. - The second `Mapper` values are in the entries `[103:303] - For this example, `param_range_list_from(cls=AbstractMapper)` therefore returns the + For this example, `param_range_list_from(cls=Mapper)` therefore returns the list `[[3, 103], [103, 303]]`. Parameters diff --git a/autoarray/inversion/linear_obj/func_list.py b/autoarray/inversion/linear_obj/func_list.py index 82393835f..7301a7bc2 100644 --- a/autoarray/inversion/linear_obj/func_list.py +++ b/autoarray/inversion/linear_obj/func_list.py @@ -7,7 +7,7 @@ from autoarray.inversion.linear_obj.neighbors import Neighbors from autoarray.inversion.linear_obj.unique_mappings import UniqueMappings from autoarray.inversion.regularization.abstract import AbstractRegularization -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.type import Grid1D2DLike @@ -16,7 +16,7 @@ def __init__( self, grid: Grid1D2DLike, regularization: Optional[AbstractRegularization], - settings=SettingsInversion(), + settings=Settings(), xp=np, ): """ diff --git a/autoarray/inversion/pixelization/__init__.py b/autoarray/inversion/mappers/__init__.py similarity index 100% rename from autoarray/inversion/pixelization/__init__.py rename to autoarray/inversion/mappers/__init__.py diff --git a/autoarray/inversion/pixelization/mappers/abstract.py b/autoarray/inversion/mappers/abstract.py similarity index 83% rename from autoarray/inversion/pixelization/mappers/abstract.py rename to autoarray/inversion/mappers/abstract.py index 7a29c61a9..706cfea95 100644 --- a/autoarray/inversion/pixelization/mappers/abstract.py +++ b/autoarray/inversion/mappers/abstract.py @@ -8,35 +8,31 @@ from autoarray.inversion.linear_obj.linear_obj import LinearObj from autoarray.inversion.linear_obj.func_list import UniqueMappings from autoarray.inversion.linear_obj.neighbors import Neighbors -from autoarray.inversion.pixelization.border_relocator import BorderRelocator -from autoarray.inversion.pixelization.mappers.mapper_grids import MapperGrids from autoarray.inversion.regularization.abstract import AbstractRegularization -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.structures.arrays.uniform_2d import Array2D -from autoarray.structures.grids.uniform_2d import Grid2D -from autoarray.structures.mesh.abstract_2d import Abstract2DMesh -from autoarray.inversion.pixelization.mappers import mapper_util -from autoarray.inversion.pixelization.mappers import mapper_numba_util +from autoarray.inversion.mappers import mapper_util +from autoarray.inversion.mappers import mapper_numba_util -class AbstractMapper(LinearObj): +class Mapper(LinearObj): def __init__( self, - mapper_grids: MapperGrids, - regularization: Optional[AbstractRegularization], - border_relocator: BorderRelocator, - settings: SettingsInversion = SettingsInversion(), + interpolator, + regularization: Optional[AbstractRegularization] = None, + settings: Settings = None, + image_plane_mesh_grid=None, preloads=None, xp=np, ): """ To understand a `Mapper` one must be familiar `Mesh` objects and the `mesh` and `pixelization` packages, where - the four grids grouped in a `MapperGrids` object are explained (`image_plane_data_grid`, `source_plane_data_grid`, + the four grids are explained (`image_plane_data_grid`, `source_plane_data_grid`, `image_plane_mesh_grid`,`source_plane_mesh_grid`) If you are unfamliar withe above objects, read through the docstrings of the `pixelization`, `mesh` and - `mapper_grids` packages. + `image_mesh` packages. A `Mapper` determines the mappings between the masked data grid's pixels (`image_plane_data_grid` and `source_plane_data_grid`) and the pxelization's pixels (`image_plane_mesh_grid` and `source_plane_mesh_grid`). @@ -76,59 +72,68 @@ def __init__( Parameters ---------- - mapper_grids - An object containing the data grid and mesh grid in both the data-frame and source-frame used by the - mapper to map data-points to linear object parameters. + source_plane_data_grid + A 2D grid of (y,x) coordinates associated with the unmasked 2D data after it has been transformed to the + `source` reference frame. + source_plane_mesh_grid + The 2D grid of (y,x) centres of every pixelization pixel in the `source` frame. + image_plane_mesh_grid + The sparse set of (y,x) coordinates computed from the unmasked data in the `data` frame. This has a + transformation applied to it to create the `source_plane_mesh_grid`. + adapt_data + An image which is used to determine the `image_plane_mesh_grid` and therefore adapt the distribution of + pixels of the Delaunay grid to the data it discretizes. + mesh_weight_map + The weight map used to weight the creation of the rectangular mesh grid, which is used for the + `RectangularBrightness` mesh which adapts the size of its pixels to where the source is reconstructed. regularization The regularization scheme which may be applied to this linear object in order to smooth its solution, which for a mapper smooths neighboring pixels on the mesh. - border_relocator - The border relocator, which relocates coordinates outside the border of the source-plane data grid to its - edge. """ super().__init__(regularization=regularization, xp=xp) - self.border_relocator = border_relocator - self.mapper_grids = mapper_grids + self.interpolator = interpolator + + self.image_plane_mesh_grid = image_plane_mesh_grid self.preloads = preloads - self.settings = settings + self.settings = settings or Settings() @property def params(self) -> int: - return self.source_plane_mesh_grid.pixels + return self.source_plane_mesh_grid.shape[0] @property def pixels(self) -> int: return self.params @property - def source_plane_data_grid(self) -> Grid2D: - return self.mapper_grids.source_plane_data_grid + def mask(self): + return self.source_plane_data_grid.mask @property - def source_plane_mesh_grid(self) -> Abstract2DMesh: - return self.mapper_grids.source_plane_mesh_grid + def mesh_geometry(self): + return self.interpolator.mesh_geometry @property - def image_plane_mesh_grid(self) -> Grid2D: - return self.mapper_grids.image_plane_mesh_grid + def over_sampler(self): + return self.source_plane_data_grid.over_sampler @property - def over_sampler(self): - return self.mapper_grids.source_plane_data_grid.over_sampler + def source_plane_data_grid(self): + return self.interpolator.data_grid @property - def adapt_data(self) -> np.ndarray: - return self.mapper_grids.adapt_data + def source_plane_mesh_grid(self): + return self.interpolator.mesh_grid @property - def neighbors(self) -> Neighbors: - return self.source_plane_mesh_grid.neighbors + def adapt_data(self): + return self.interpolator.adapt_data @property - def pix_sub_weights(self) -> "PixSubWeights": - raise NotImplementedError + def neighbors(self) -> Neighbors: + return self.mesh_geometry.neighbors @property def pix_indexes_for_sub_slim_index(self) -> np.ndarray: @@ -144,7 +149,7 @@ def pix_indexes_for_sub_slim_index(self) -> np.ndarray: - `pix_indexes_for_sub_slim_index[2, 0] = 4`: The data's third (index 2) sub-pixel maps to the pixelization's fifth (index 4) pixel. """ - return self.pix_sub_weights.mappings + return self.interpolator.mappings @property def pix_sizes_for_sub_slim_index(self) -> np.ndarray: @@ -160,7 +165,7 @@ def pix_sizes_for_sub_slim_index(self) -> np.ndarray: - `pix_sizes_for_sub_slim_index[2] = 4`: The data's third (index 2) sub-pixel maps to 4 pixels in the pixelization. """ - return self.pix_sub_weights.sizes + return self.interpolator.sizes @property def pix_weights_for_sub_slim_index(self) -> np.ndarray: @@ -177,7 +182,7 @@ def pix_weights_for_sub_slim_index(self) -> np.ndarray: - `pix_weights_for_sub_slim_index[2, 0] = 0.2`: The data's third (index 2) sub-pixel mapping to the pixelization's fifth (index 4) pixel has an interpolation weight of 0.2. """ - return self.pix_sub_weights.weights + return self.interpolator.weights @property def slim_index_for_sub_slim_index(self) -> np.ndarray: @@ -322,7 +327,7 @@ def sparse_triplets_data(self): pix_indexes_for_sub=self.pix_indexes_for_sub_slim_index, pix_weights_for_sub=self.pix_weights_for_sub_slim_index, slim_index_for_sub=self.slim_index_for_sub_slim_index, - fft_index_for_masked_pixel=self.mapper_grids.mask.fft_index_for_masked_pixel, + fft_index_for_masked_pixel=self.mask.fft_index_for_masked_pixel, sub_fraction_slim=self.over_sampler.sub_fraction.array, xp=self._xp, ) @@ -378,7 +383,7 @@ def sparse_triplets_curvature(self): pix_indexes_for_sub=self.pix_indexes_for_sub_slim_index, pix_weights_for_sub=self.pix_weights_for_sub_slim_index, slim_index_for_sub=self.slim_index_for_sub_slim_index, - fft_index_for_masked_pixel=self.mapper_grids.mask.fft_index_for_masked_pixel, + fft_index_for_masked_pixel=self.mask.fft_index_for_masked_pixel, sub_fraction_slim=self.over_sampler.sub_fraction.array, xp=self._xp, return_rows_slim=False, @@ -528,28 +533,23 @@ def extent_from( extent=self.source_plane_mesh_grid.geometry.extent ) + @property + def image_plane_data_grid(self): + return self.mask.derive_grid.unmasked -class PixSubWeights: - def __init__(self, mappings: np.ndarray, sizes: np.ndarray, weights: np.ndarray): - """ - Packages the mappings, sizes and weights of every data pixel to pixelization pixels, which are computed - from associated ``Mapper`` properties.. + @property + def mesh_pixels_per_image_pixels(self): - The need to store separately the mappings and sizes is so that the `sizes` can be easy iterated over when - perform calculations for efficiency. + from autoarray.structures.grids import grid_2d_util - Parameters - ---------- - mappings - The mapping of every data pixel, given its `sub_slim_index`, to its corresponding pixelization mesh - pixels, given their `pix_indexes` (corresponds to the ``Mapper`` - property ``pix_indexes_for_sub_slim_index``) - sizes - The number of mappings of every data pixel to pixelization mesh pixels (corresponds to the ``Mapper`` - property ``pix_sizes_for_sub_slim_index``). - weights - The interpolation weights of every data pixel's pixelization pixel mapping. - """ - self.mappings = mappings - self.sizes = sizes - self.weights = weights + mesh_pixels_per_image_pixels = grid_2d_util.grid_pixels_in_mask_pixels_from( + grid=np.array(self.image_plane_mesh_grid), + shape_native=self.mask.shape_native, + pixel_scales=self.mask.pixel_scales, + origin=self.mask.origin, + ) + + return Array2D( + values=mesh_pixels_per_image_pixels, + mask=self.mask, + ) diff --git a/autoarray/inversion/pixelization/mappers/mapper_numba_util.py b/autoarray/inversion/mappers/mapper_numba_util.py similarity index 100% rename from autoarray/inversion/pixelization/mappers/mapper_numba_util.py rename to autoarray/inversion/mappers/mapper_numba_util.py diff --git a/autoarray/inversion/pixelization/mappers/mapper_util.py b/autoarray/inversion/mappers/mapper_util.py similarity index 55% rename from autoarray/inversion/pixelization/mappers/mapper_util.py rename to autoarray/inversion/mappers/mapper_util.py index 385f2190f..8c5fd8586 100644 --- a/autoarray/inversion/pixelization/mappers/mapper_util.py +++ b/autoarray/inversion/mappers/mapper_util.py @@ -3,361 +3,6 @@ from typing import Tuple -def forward_interp(xp, yp, x): - - import jax - import jax.numpy as jnp - - return jax.vmap(jnp.interp, in_axes=(1, 1, 1, None, None), out_axes=(1))( - x, xp, yp, 0, 1 - ) - - -def reverse_interp(xp, yp, x): - import jax - import jax.numpy as jnp - - return jax.vmap(jnp.interp, in_axes=(1, 1, 1), out_axes=(1))(x, xp, yp) - - -def forward_interp_np(xp, yp, x): - """ - xp: (N, M) - yp: (N, M) - x : (M,) ← one x per column - """ - - if yp.ndim == 1 and xp.ndim == 2: - yp = np.broadcast_to(yp[:, None], xp.shape) - - K, M = x.shape - - out = np.empty((K, 2), dtype=xp.dtype) - - for j in range(2): - out[:, j] = np.interp(x[:, j], xp[:, j], yp[:, j], left=0, right=1) - - return out - - -def reverse_interp_np(xp, yp, x): - """ - xp : (N,) or (N, M) - yp : (N, M) - x : (K, M) query points per column - """ - - # Ensure xp is 2D: (N, M) - if xp.ndim == 1 and yp.ndim == 2: # (N, 1) - xp = np.broadcast_to(xp[:, None], yp.shape) - - # Shapes - K, M = x.shape - - # Output - out = np.empty((K, 2), dtype=yp.dtype) - - # Column-wise interpolation (cannot avoid this loop in pure NumPy) - for j in range(2): - out[:, j] = np.interp(x[:, j], xp[:, j], yp[:, j]) - - return out - - -def create_transforms(traced_points, mesh_weight_map=None, xp=np): - - N = traced_points.shape[0] # // 2 - - if mesh_weight_map is None: - t = xp.arange(1, N + 1) / (N + 1) - t = xp.stack([t, t], axis=1) - sort_points = xp.sort(traced_points, axis=0) # [::2] - else: - sdx = xp.argsort(traced_points, axis=0) - sort_points = xp.take_along_axis(traced_points, sdx, axis=0) - t = xp.stack([mesh_weight_map, mesh_weight_map], axis=1) - t = xp.take_along_axis(t, sdx, axis=0) - t = xp.cumsum(t, axis=0) - - if xp.__name__.startswith("jax"): - transform = partial(forward_interp, sort_points, t) - inv_transform = partial(reverse_interp, t, sort_points) - return transform, inv_transform - - transform = partial(forward_interp_np, sort_points, t) - inv_transform = partial(reverse_interp_np, t, sort_points) - return transform, inv_transform - - -def adaptive_rectangular_transformed_grid_from( - source_plane_data_grid, grid, mesh_weight_map=None, xp=np -): - - mu = source_plane_data_grid.mean(axis=0) - scale = source_plane_data_grid.std(axis=0).min() - source_grid_scaled = (source_plane_data_grid - mu) / scale - - transform, inv_transform = create_transforms( - source_grid_scaled, mesh_weight_map=mesh_weight_map, xp=xp - ) - - def inv_full(U): - return inv_transform(U) * scale + mu - - return inv_full(grid) - - -def adaptive_rectangular_areas_from( - source_grid_shape, source_plane_data_grid, mesh_weight_map=None, xp=np -): - - edges_y = xp.linspace(1, 0, source_grid_shape[0] + 1) - edges_x = xp.linspace(0, 1, source_grid_shape[1] + 1) - - mu = source_plane_data_grid.mean(axis=0) - scale = source_plane_data_grid.std(axis=0).min() - source_grid_scaled = (source_plane_data_grid - mu) / scale - - transform, inv_transform = create_transforms( - source_grid_scaled, mesh_weight_map=mesh_weight_map, xp=xp - ) - - def inv_full(U): - return inv_transform(U) * scale + mu - - pixel_edges = inv_full(xp.stack([edges_y, edges_x]).T) - pixel_lengths = xp.diff(pixel_edges, axis=0).squeeze() # shape (N_source, 2) - - dy = pixel_lengths[:, 0] - dx = pixel_lengths[:, 1] - - return xp.abs(xp.outer(dy, dx).flatten()) - - -def adaptive_rectangular_mappings_weights_via_interpolation_from( - source_grid_size: int, - source_plane_data_grid, - source_plane_data_grid_over_sampled, - mesh_weight_map=None, - xp=np, -): - """ - Compute bilinear interpolation indices and weights for mapping an oversampled - source-plane grid onto a regular rectangular pixelization. - - This function takes a set of irregularly-sampled source-plane coordinates and - builds an adaptive mapping onto a `source_grid_size x source_grid_size` rectangular - pixelization using bilinear interpolation. The interpolation is expressed as: - - f(x, y) ≈ w_bl * f(ix_down, iy_down) + - w_br * f(ix_up, iy_down) + - w_tl * f(ix_down, iy_up) + - w_tr * f(ix_up, iy_up) - - where `(ix_down, ix_up, iy_down, iy_up)` are the integer grid coordinates - surrounding the continuous position `(x, y)`. - - Steps performed: - 1. Normalize the source-plane grid by subtracting its mean and dividing by - the minimum axis standard deviation (to balance scaling). - 2. Construct forward/inverse transforms which map the grid into the unit square [0,1]^2. - 3. Transform the oversampled source-plane grid into [0,1]^2, then scale it - to index space `[0, source_grid_size)`. - 4. Compute floor/ceil along x and y axes to find the enclosing rectangular cell. - 5. Build the four corner indices: bottom-left (bl), bottom-right (br), - top-left (tl), and top-right (tr). - 6. Flatten the 2D indices into 1D indices suitable for scatter operations, - with a flipped row-major convention: row = source_grid_size - i, col = j. - 7. Compute bilinear interpolation weights (`w_bl, w_br, w_tl, w_tr`). - 8. Return arrays of flattened indices and weights of shape `(N, 4)`, where - `N` is the number of oversampled coordinates. - - Parameters - ---------- - source_grid_size : int - The number of pixels along one dimension of the rectangular pixelization. - The grid is square: (source_grid_size x source_grid_size). - source_plane_data_grid : (M, 2) ndarray - The base source-plane coordinates, used to define normalization and transforms. - source_plane_data_grid_over_sampled : (N, 2) ndarray - Oversampled source-plane coordinates to be interpolated onto the rectangular grid. - mesh_weight_map - The weight map used to weight the creation of the rectangular mesh grid, which is used for the - `RectangularBrightness` mesh which adapts the size of its pixels to where the source is reconstructed. - - Returns - ------- - flat_indices : (N, 4) int ndarray - The flattened indices of the four neighboring pixel corners for each oversampled point. - Order: [bl, br, tl, tr]. - weights : (N, 4) float ndarray - The bilinear interpolation weights for each of the four neighboring pixels. - Order: [w_bl, w_br, w_tl, w_tr]. - """ - - # --- Step 1. Normalize grid --- - mu = source_plane_data_grid.mean(axis=0) - scale = source_plane_data_grid.std(axis=0).min() - source_grid_scaled = (source_plane_data_grid - mu) / scale - - # --- Step 2. Build transforms --- - transform, inv_transform = create_transforms( - source_grid_scaled, mesh_weight_map=mesh_weight_map, xp=xp - ) - - # --- Step 3. Transform oversampled grid into index space --- - grid_over_sampled_scaled = (source_plane_data_grid_over_sampled - mu) / scale - grid_over_sampled_transformed = transform(grid_over_sampled_scaled) - grid_over_index = (source_grid_size - 3) * grid_over_sampled_transformed + 1 - - # --- Step 4. Floor/ceil indices --- - ix_down = xp.floor(grid_over_index[:, 0]) - ix_up = xp.ceil(grid_over_index[:, 0]) - iy_down = xp.floor(grid_over_index[:, 1]) - iy_up = xp.ceil(grid_over_index[:, 1]) - - # --- Step 5. Four corners --- - idx_tl = xp.stack([ix_up, iy_down], axis=1) - idx_tr = xp.stack([ix_up, iy_up], axis=1) - idx_br = xp.stack([ix_down, iy_up], axis=1) - idx_bl = xp.stack([ix_down, iy_down], axis=1) - - # --- Step 6. Flatten indices --- - def flatten(idx, n): - row = n - idx[:, 0] - col = idx[:, 1] - return row * n + col - - flat_tl = flatten(idx_tl, source_grid_size) - flat_tr = flatten(idx_tr, source_grid_size) - flat_bl = flatten(idx_bl, source_grid_size) - flat_br = flatten(idx_br, source_grid_size) - - flat_indices = xp.stack([flat_tl, flat_tr, flat_bl, flat_br], axis=1).astype( - "int64" - ) - - # --- Step 7. Bilinear interpolation weights --- - t_row = (grid_over_index[:, 0] - ix_down) / (ix_up - ix_down + 1e-12) - t_col = (grid_over_index[:, 1] - iy_down) / (iy_up - iy_down + 1e-12) - - # Weights - w_tl = (1 - t_row) * (1 - t_col) - w_tr = (1 - t_row) * t_col - w_bl = t_row * (1 - t_col) - w_br = t_row * t_col - weights = xp.stack([w_tl, w_tr, w_bl, w_br], axis=1) - - return flat_indices, weights - - -def rectangular_mappings_weights_via_interpolation_from( - shape_native: Tuple[int, int], - source_plane_data_grid: np.ndarray, - source_plane_mesh_grid: np.ndarray, - xp=np, -): - """ - Compute bilinear interpolation weights and corresponding rectangular mesh indices for an irregular grid. - - Given a flattened regular rectangular mesh grid and an irregular grid of data points, this function - determines for each irregular point: - - the indices of the 4 nearest rectangular mesh pixels (top-left, top-right, bottom-left, bottom-right), and - - the bilinear interpolation weights with respect to those pixels. - - The function supports JAX and is compatible with JIT compilation. - - Parameters - ---------- - shape_native - The shape (Ny, Nx) of the original rectangular mesh grid before flattening. - source_plane_data_grid - The irregular grid of (y, x) points to interpolate. - source_plane_mesh_grid - The flattened regular rectangular mesh grid of (y, x) coordinates. - - Returns - ------- - mappings : np.ndarray of shape (N, 4) - Indices of the four nearest rectangular mesh pixels in the flattened mesh grid. - Order is: top-left, top-right, bottom-left, bottom-right. - weights : np.ndarray of shape (N, 4) - Bilinear interpolation weights corresponding to the four nearest mesh pixels. - - Notes - ----- - - Assumes the mesh grid is uniformly spaced. - - The weights sum to 1 for each irregular point. - - Uses bilinear interpolation in the (y, x) coordinate system. - """ - source_plane_mesh_grid = source_plane_mesh_grid.reshape(*shape_native, 2) - - # Assume mesh is shaped (Ny, Nx, 2) - Ny, Nx = source_plane_mesh_grid.shape[:2] - - # Get mesh spacings and lower corner - y_coords = source_plane_mesh_grid[:, 0, 0] # shape (Ny,) - x_coords = source_plane_mesh_grid[0, :, 1] # shape (Nx,) - - dy = y_coords[1] - y_coords[0] - dx = x_coords[1] - x_coords[0] - - y_min = y_coords[0] - x_min = x_coords[0] - - # shape (N_irregular, 2) - irregular = source_plane_data_grid - - # Compute normalized mesh coordinates (floating indices) - fy = (irregular[:, 0] - y_min) / dy - fx = (irregular[:, 1] - x_min) / dx - - # Integer indices of top-left corners - ix = xp.floor(fx).astype(xp.int32) - iy = xp.floor(fy).astype(xp.int32) - - # Clip to stay within bounds - ix = xp.clip(ix, 0, Nx - 2) - iy = xp.clip(iy, 0, Ny - 2) - - # Local coordinates inside the cell (0 <= tx, ty <= 1) - tx = fx - ix - ty = fy - iy - - # Bilinear weights - w00 = (1 - tx) * (1 - ty) - w10 = tx * (1 - ty) - w01 = (1 - tx) * ty - w11 = tx * ty - - weights = xp.stack([w00, w10, w01, w11], axis=1) # shape (N_irregular, 4) - - # Compute indices of 4 surrounding pixels in the flattened mesh - i00 = iy * Nx + ix - i10 = iy * Nx + (ix + 1) - i01 = (iy + 1) * Nx + ix - i11 = (iy + 1) * Nx + (ix + 1) - - mappings = xp.stack([i00, i10, i01, i11], axis=1) # shape (N_irregular, 4) - - return mappings, weights - - -def nearest_pixelization_index_for_slim_index_from_kdtree(grid, mesh_grid): - from scipy.spatial import cKDTree - - kdtree = cKDTree(mesh_grid) - - sparse_index_for_slim_index = [] - - for i in range(grid.shape[0]): - input_point = [grid[i, [0]], grid[i, 1]] - index = kdtree.query(input_point)[1] - sparse_index_for_slim_index.append(index) - - return sparse_index_for_slim_index - - def adaptive_pixel_signals_from( pixels: int, pixel_weights: np.ndarray, diff --git a/autoarray/inversion/pixelization/mappers/__init__.py b/autoarray/inversion/mesh/__init__.py similarity index 100% rename from autoarray/inversion/pixelization/mappers/__init__.py rename to autoarray/inversion/mesh/__init__.py diff --git a/autoarray/inversion/pixelization/border_relocator.py b/autoarray/inversion/mesh/border_relocator.py similarity index 100% rename from autoarray/inversion/pixelization/border_relocator.py rename to autoarray/inversion/mesh/border_relocator.py diff --git a/autoarray/inversion/pixelization/image_mesh/__init__.py b/autoarray/inversion/mesh/image_mesh/__init__.py similarity index 100% rename from autoarray/inversion/pixelization/image_mesh/__init__.py rename to autoarray/inversion/mesh/image_mesh/__init__.py diff --git a/autoarray/inversion/pixelization/image_mesh/abstract.py b/autoarray/inversion/mesh/image_mesh/abstract.py similarity index 100% rename from autoarray/inversion/pixelization/image_mesh/abstract.py rename to autoarray/inversion/mesh/image_mesh/abstract.py diff --git a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py b/autoarray/inversion/mesh/image_mesh/abstract_weighted.py similarity index 93% rename from autoarray/inversion/pixelization/image_mesh/abstract_weighted.py rename to autoarray/inversion/mesh/image_mesh/abstract_weighted.py index 5aaa119c7..3c7ef4219 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py +++ b/autoarray/inversion/mesh/image_mesh/abstract_weighted.py @@ -1,6 +1,6 @@ import numpy as np -from autoarray.inversion.pixelization.image_mesh.abstract import AbstractImageMesh +from autoarray.inversion.mesh.image_mesh.abstract import AbstractImageMesh class AbstractImageMeshWeighted(AbstractImageMesh): diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert.py b/autoarray/inversion/mesh/image_mesh/hilbert.py similarity index 96% rename from autoarray/inversion/pixelization/image_mesh/hilbert.py rename to autoarray/inversion/mesh/image_mesh/hilbert.py index cf6b0a933..941e62b81 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert.py +++ b/autoarray/inversion/mesh/image_mesh/hilbert.py @@ -6,7 +6,7 @@ from autoarray.structures.grids.uniform_2d import Grid2D from autoarray.mask.mask_2d import Mask2D -from autoarray.inversion.pixelization.image_mesh.abstract_weighted import ( +from autoarray.inversion.mesh.image_mesh.abstract_weighted import ( AbstractImageMeshWeighted, ) from autoarray.structures.grids.irregular_2d import Grid2DIrregular diff --git a/autoarray/inversion/pixelization/image_mesh/kmeans.py b/autoarray/inversion/mesh/image_mesh/kmeans.py similarity index 93% rename from autoarray/inversion/pixelization/image_mesh/kmeans.py rename to autoarray/inversion/mesh/image_mesh/kmeans.py index 0ac630920..8e70b87e0 100644 --- a/autoarray/inversion/pixelization/image_mesh/kmeans.py +++ b/autoarray/inversion/mesh/image_mesh/kmeans.py @@ -5,11 +5,11 @@ from autoarray.mask.mask_2d import Mask2D -from autoarray.inversion.pixelization.image_mesh.abstract_weighted import ( +from autoarray.inversion.mesh.image_mesh.abstract_weighted import ( AbstractImageMeshWeighted, ) from autoarray.structures.grids.irregular_2d import Grid2DIrregular -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray import exc diff --git a/autoarray/inversion/pixelization/image_mesh/overlay.py b/autoarray/inversion/mesh/image_mesh/overlay.py similarity index 95% rename from autoarray/inversion/pixelization/image_mesh/overlay.py rename to autoarray/inversion/mesh/image_mesh/overlay.py index 75edf6c9b..c70ec840d 100644 --- a/autoarray/inversion/pixelization/image_mesh/overlay.py +++ b/autoarray/inversion/mesh/image_mesh/overlay.py @@ -2,9 +2,9 @@ from typing import Optional from autoarray.mask.mask_2d import Mask2D -from autoarray.inversion.pixelization.image_mesh.abstract import AbstractImageMesh +from autoarray.inversion.mesh.image_mesh.abstract import AbstractImageMesh from autoarray.structures.grids.irregular_2d import Grid2DIrregular -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings from autoarray.geometry import geometry_util from autoarray.structures.grids import grid_2d_util diff --git a/autoarray/structures/mesh/__init__.py b/autoarray/inversion/mesh/interpolator/__init__.py similarity index 100% rename from autoarray/structures/mesh/__init__.py rename to autoarray/inversion/mesh/interpolator/__init__.py diff --git a/autoarray/inversion/mesh/interpolator/abstract.py b/autoarray/inversion/mesh/interpolator/abstract.py new file mode 100644 index 000000000..df8790365 --- /dev/null +++ b/autoarray/inversion/mesh/interpolator/abstract.py @@ -0,0 +1,49 @@ +import numpy as np + + +class AbstractInterpolator: + + def __init__( + self, + mesh, + mesh_grid, + data_grid, + adapt_data: np.ndarray = None, + preloads=None, + xp=np, + ): + self.mesh = mesh + self.mesh_grid = mesh_grid + self.data_grid = data_grid + self.adapt_data = adapt_data + self.preloads = preloads + self.use_jax = xp is not np + + @property + def _xp(self): + if self.use_jax: + import jax.numpy as jnp + + return jnp + return np + + @property + def _mappings_sizes_weights(self): + raise NotImplementedError( + "Subclasses of AbstractInterpolator must implement the _mappings_sizes_weights property." + ) + + @property + def mappings(self): + mappings, _, _ = self._mappings_sizes_weights + return mappings + + @property + def sizes(self): + _, sizes, _ = self._mappings_sizes_weights + return sizes + + @property + def weights(self): + _, _, weights = self._mappings_sizes_weights + return weights diff --git a/autoarray/structures/mesh/delaunay_2d.py b/autoarray/inversion/mesh/interpolator/delaunay.py similarity index 52% rename from autoarray/structures/mesh/delaunay_2d.py rename to autoarray/inversion/mesh/interpolator/delaunay.py index 961944b75..deff4e981 100644 --- a/autoarray/structures/mesh/delaunay_2d.py +++ b/autoarray/inversion/mesh/interpolator/delaunay.py @@ -1,19 +1,16 @@ import numpy as np import scipy.spatial from scipy.spatial import cKDTree, Delaunay, Voronoi -from typing import List, Union, Optional, Tuple from autoconf import cached_property -from autoarray.geometry.geometry_2d_irregular import Geometry2DIrregular -from autoarray.structures.mesh.abstract_2d import Abstract2DMesh -from autoarray.structures.arrays.uniform_2d import Array2D -from autoarray.inversion.linear_obj.neighbors import Neighbors +from autoarray.inversion.mesh.interpolator.abstract import AbstractInterpolator +from autoarray.inversion.regularization.regularization_util import ( + split_points_from, +) -from autoarray import exc - -def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): +def scipy_delaunay(points_np, query_points_np, areas_factor): """Compute Delaunay simplices (simplices_padded) and Voronoi areas in one call.""" max_simplices = 2 * points_np.shape[0] @@ -32,32 +29,19 @@ def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): simplex_idx = tri.find_simplex(query_points_np).astype(np.int32) # (Q,) mappings = pix_indexes_for_sub_slim_index_delaunay_from( - source_plane_data_grid=query_points_np, + data_grid=query_points_np, simplex_index_for_sub_slim_index=simplex_idx, pix_indexes_for_simplex_index=simplices, delaunay_points=points_np, ) - # ---------- Voronoi or Barycentric Areas used to weight split points ---------- - - if use_voronoi_areas: - - areas = voronoi_areas_numpy(points) - - max_area = np.percentile(areas, 90.0) - if max_area <= 0: - max_area = 0.1 - - areas[areas == -1] = max_area - areas[areas > max_area] = max_area - - else: + # ---------- Barycentric Areas used to weight split points ---------- - areas = barycentric_dual_area_from( - points, - simplices, - xp=np, - ) + areas = barycentric_dual_area_from( + points, + simplices, + xp=np, + ) split_point_areas = areas_factor * np.sqrt(areas) @@ -71,7 +55,7 @@ def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): split_points_idx = tri.find_simplex(split_points) splitted_mappings = pix_indexes_for_sub_slim_index_delaunay_from( - source_plane_data_grid=split_points, + data_grid=split_points, simplex_index_for_sub_slim_index=split_points_idx, pix_indexes_for_simplex_index=simplices, delaunay_points=points_np, @@ -80,7 +64,7 @@ def scipy_delaunay(points_np, query_points_np, use_voronoi_areas, areas_factor): return points, simplices_padded, mappings, split_points, splitted_mappings -def jax_delaunay(points, query_points, use_voronoi_areas, areas_factor=0.5): +def jax_delaunay(points, query_points, areas_factor=0.5): import jax import jax.numpy as jnp @@ -96,7 +80,7 @@ def jax_delaunay(points, query_points, use_voronoi_areas, areas_factor=0.5): return jax.pure_callback( lambda points, qpts: scipy_delaunay( - np.asarray(points), np.asarray(qpts), use_voronoi_areas, areas_factor + np.asarray(points), np.asarray(qpts), areas_factor ), ( points_shape, @@ -168,147 +152,14 @@ def barycentric_dual_area_from( return dual_area -def voronoi_areas_numpy(points, qhull_options="Qbb Qc Qx Qm Q12 Pp"): - """ - Compute Voronoi cell areas with a fully optimized pure-NumPy pipeline. - Exact match to the per-cell SciPy Voronoi loop but much faster. - """ - vor = Voronoi(points, qhull_options=qhull_options) - - vertices = vor.vertices - point_region = vor.point_region - regions = vor.regions - N = len(point_region) - - # ------------------------------------------------------------ - # 1) Collect all region lists in one go (list comprehension is fast) - # ------------------------------------------------------------ - region_lists = [regions[r] for r in point_region] - - # Precompute which regions are unbounded (vectorized test) - unbounded = np.array([(-1 in r) for r in region_lists], dtype=bool) - - # Filter only bounded region vertex indices - clean_regions = [ - np.asarray([v for v in r if v != -1], dtype=int) for r in region_lists - ] - - # Compute lengths once - lengths = np.array([len(r) for r in clean_regions], dtype=int) - max_len = lengths.max() - - # ------------------------------------------------------------ - # 2) Build padded idx + mask in a vectorized-like way - # - # Instead of doing Python work inside the loop, we pre-pack - # the flattened data and then reshape. - # ------------------------------------------------------------ - idx = np.full((N, max_len), -1, dtype=int) - mask = np.zeros((N, max_len), dtype=bool) - - # Single loop remaining: extremely cheap - for i, (r, L) in enumerate(zip(clean_regions, lengths)): - if L: - idx[i, :L] = r - mask[i, :L] = True - - # ------------------------------------------------------------ - # 3) Gather polygon vertices (vectorized) - # ------------------------------------------------------------ - safe_idx = idx.clip(min=0) - verts = vertices[safe_idx] # (N, max_len, 2) - - # Extract x, y with masked invalid entries zeroed - x = np.where(mask, verts[..., 1], 0.0) - y = np.where(mask, verts[..., 0], 0.0) - - # ------------------------------------------------------------ - # 4) Vectorized "previous index" per polygon - # ------------------------------------------------------------ - safe_lengths = np.where(lengths == 0, 1, lengths) - j = np.arange(max_len) - prev = (j[None, :] - 1) % safe_lengths[:, None] - - # Efficient take-along-axis - x_prev = np.take_along_axis(x, prev, axis=1) - y_prev = np.take_along_axis(y, prev, axis=1) - - # ------------------------------------------------------------ - # 5) Shoelace vectorized - # ------------------------------------------------------------ - cross = x * y_prev - y * x_prev - areas = 0.5 * np.abs(cross.sum(axis=1)) - - # ------------------------------------------------------------ - # 6) Mark unbounded regions - # ------------------------------------------------------------ - areas[unbounded] = -1.0 - - return areas - - -def split_points_from(points, area_weights, xp=np): - """ - points : (N, 2) - areas : (N,) - xp : np or jnp - - Returns (4*N, 2) - """ - - N = points.shape[0] - offsets = area_weights - - x = points[:, 0] - y = points[:, 1] - - # Allocate output (N, 4, 2) - out = xp.zeros((N, 4, 2), dtype=points.dtype) - - if xp.__name__.startswith("jax"): - # ---------------------------- - # JAX → use .at[] updates - # ---------------------------- - out = out.at[:, 0, 0].set(x + offsets) - out = out.at[:, 0, 1].set(y) - - out = out.at[:, 1, 0].set(x - offsets) - out = out.at[:, 1, 1].set(y) - - out = out.at[:, 2, 0].set(x) - out = out.at[:, 2, 1].set(y + offsets) - - out = out.at[:, 3, 0].set(x) - out = out.at[:, 3, 1].set(y - offsets) - - else: - - # ---------------------------- - # NumPy → direct assignment OK - # ---------------------------- - out[:, 0, 0] = x + offsets - out[:, 0, 1] = y - - out[:, 1, 0] = x - offsets - out[:, 1, 1] = y - - out[:, 2, 0] = x - out[:, 2, 1] = y + offsets - - out[:, 3, 0] = x - out[:, 3, 1] = y - offsets - - return out.reshape((N * 4, 2)) - - def pix_indexes_for_sub_slim_index_delaunay_from( - source_plane_data_grid, # (N_sub, 2) + data_grid, # (N_sub, 2) simplex_index_for_sub_slim_index, # (N_sub,) pix_indexes_for_simplex_index, # (M, 3) delaunay_points, # (N_pix, 2) ): - N_sub = source_plane_data_grid.shape[0] + N_sub = data_grid.shape[0] inside_mask = simplex_index_for_sub_slim_index >= 0 outside_mask = ~inside_mask @@ -331,7 +182,7 @@ def pix_indexes_for_sub_slim_index_delaunay_from( # --------------------------- if outside_mask.any(): tree = cKDTree(delaunay_points) - _, idx = tree.query(source_plane_data_grid[outside_mask], k=1) + _, idx = tree.query(data_grid[outside_mask], k=1) out[outside_mask, 0] = idx.astype(np.int32) out = out.astype(np.int32) @@ -367,7 +218,7 @@ def scipy_delaunay_matern(points_np, query_points_np): simplex_idx = tri.find_simplex(query_points_np).astype(np.int32) # (Q,) mappings = pix_indexes_for_sub_slim_index_delaunay_from( - source_plane_data_grid=query_points_np, + data_grid=query_points_np, simplex_index_for_sub_slim_index=simplex_idx, pix_indexes_for_simplex_index=simplices, delaunay_points=points_np, @@ -400,6 +251,93 @@ def jax_delaunay_matern(points, query_points): ) +def triangle_area_xp(c0, c1, c2, xp): + """ + Twice triangle area using vector cross product magnitude. + Calling via xp ensures NumPy or JAX backend operation. + """ + v0 = c1 - c0 # (..., 2) + v1 = c2 - c0 + cross = v0[..., 0] * v1[..., 1] - v0[..., 1] * v1[..., 0] + return xp.abs(cross) + + +def pixel_weights_delaunay_from( + data_grid, # (N_sub, 2) + mesh_grid, # (N_pix, 2) + pix_indexes_for_sub_slim_index, # (N_sub, 3), padded with -1 + xp=np, # backend: np (default) or jnp +): + """ + XP-compatible (NumPy/JAX) version of pixel_weights_delaunay_from. + + Computes barycentric weights for Delaunay triangle interpolation. + """ + + N_sub = pix_indexes_for_sub_slim_index.shape[0] + + # ----------------------------- + # CASE MASKS + # ----------------------------- + # If pix_indexes_for_sub_slim_index[sub][1] == -1 → NOT in simplex + has_simplex = pix_indexes_for_sub_slim_index[:, 1] != -1 # (N_sub,) + + # ----------------------------- + # GATHER TRIANGLE VERTICES + # ----------------------------- + # Clip negatives (for padded entries) so that indexing doesn't crash + safe_indices = pix_indexes_for_sub_slim_index.clip(min=0) + + # (N_sub, 3, 2) + vertices = mesh_grid[safe_indices] + + p0 = vertices[:, 0] # (N_sub, 2) + p1 = vertices[:, 1] + p2 = vertices[:, 2] + + # Query points + q = data_grid # (N_sub, 2) + + # ----------------------------- + # TRIANGLE AREAS (barycentric numerators) + # ----------------------------- + a0 = triangle_area_xp(p1, p2, q, xp) + a1 = triangle_area_xp(p0, p2, q, xp) + a2 = triangle_area_xp(p0, p1, q, xp) + + area_sum = a0 + a1 + a2 + + # (N_sub, 3) + eps = xp.asarray(1e-12, dtype=area_sum.dtype) + den = xp.where(xp.abs(area_sum) > eps, area_sum, 1.0) # avoid 0 in denominator + + weights_bary = xp.stack([a0, a1, a2], axis=1) / den[:, None] + + # If degenerate, set weights to 0 (or something sensible) + degenerate = xp.abs(area_sum) <= eps + weights_bary = xp.where(degenerate[:, None], 0.0, weights_bary) + + # ----------------------------- + # NEAREST-NEIGHBOUR CASE + # ----------------------------- + # For no-simplex: weight = [1,0,0] + weights_nn = xp.stack( + [ + xp.ones(N_sub), + xp.zeros(N_sub), + xp.zeros(N_sub), + ], + axis=1, + ) + + # ----------------------------- + # SELECT BETWEEN CASES + # ----------------------------- + pixel_weights = xp.where(has_simplex[:, None], weights_bary, weights_nn) + + return pixel_weights + + class DelaunayInterface: def __init__( @@ -423,13 +361,15 @@ def splitted_sizes(self): return self.xp.sum(self.splitted_mappings >= 0, axis=1).astype(np.int32) -class Mesh2DDelaunay(Abstract2DMesh): +class InterpolatorDelaunay(AbstractInterpolator): def __init__( self, - values: Union[np.ndarray, List], - source_plane_data_grid_over_sampled=None, + mesh, + mesh_grid, + data_grid, + adapt_data: np.ndarray = None, preloads=None, - _xp=np, + xp=np, ): """ An irregular 2D grid of (y,x) coordinates which represents both a Delaunay triangulation and Voronoi mesh. @@ -458,35 +398,27 @@ def __init__( The grid of (y,x) coordinates corresponding to the Delaunay triangle corners and Voronoi pixel centres. """ - if type(values) is list: - values = np.asarray(values) - - super().__init__(values, xp=_xp) - - self._source_plane_data_grid_over_sampled = source_plane_data_grid_over_sampled - self.preloads = preloads - - @property - def geometry(self): - shape_native_scaled = ( - np.amax(self[:, 0]).astype("float") - np.amin(self[:, 0]).astype("float"), - np.amax(self[:, 1]).astype("float") - np.amin(self[:, 1]).astype("float"), + super().__init__( + mesh=mesh, + mesh_grid=mesh_grid, + data_grid=data_grid, + adapt_data=adapt_data, + preloads=preloads, + xp=xp, ) - scaled_maxima = ( - np.amax(self[:, 0]).astype("float"), - np.amax(self[:, 1]).astype("float"), - ) + @cached_property + def mesh_geometry(self): - scaled_minima = ( - np.amin(self[:, 0]).astype("float"), - np.amin(self[:, 1]).astype("float"), + from autoarray.inversion.mesh.mesh_geometry.delaunay import ( + MeshGeometryDelaunay, ) - return Geometry2DIrregular( - shape_native_scaled=shape_native_scaled, - scaled_maxima=scaled_maxima, - scaled_minima=scaled_minima, + return MeshGeometryDelaunay( + mesh=self.mesh, + mesh_grid=self.mesh_grid, + data_grid=self.data_grid, + xp=self._xp, ) @cached_property @@ -497,7 +429,9 @@ def mesh_grid_xy(self): Therefore, this property simply converts the (y,x) grid of irregular coordinates into an (x,y) grid. """ - return self._xp.stack([self.array[:, 0], self.array[:, 1]]).T + return self._xp.stack( + [self.mesh_grid.array[:, 0], self.mesh_grid.array[:, 1]] + ).T @cached_property def delaunay(self) -> "scipy.spatial.Delaunay": @@ -514,28 +448,7 @@ def delaunay(self) -> "scipy.spatial.Delaunay": `MeshException`, which helps exception handling in the `inversion` package. """ - if self._source_plane_data_grid_over_sampled is None: - - raise ValueError( - """ - You must input the `source_plane_data_grid_over_sampled` parameter of the `Mesh2DDelaunay` object - in order to compute the Delaunay triangulation. - """ - ) - - if self.preloads is not None: - - use_voronoi_areas = self.preloads.use_voronoi_areas - areas_factor = self.preloads.areas_factor - skip_areas = self.preloads.skip_areas - - else: - - use_voronoi_areas = True - areas_factor = 0.5 - skip_areas = False - - if not skip_areas: + if not self.mesh.skip_areas: if self._xp.__name__.startswith("jax"): @@ -544,9 +457,8 @@ def delaunay(self) -> "scipy.spatial.Delaunay": points, simplices, mappings, split_points, splitted_mappings = ( jax_delaunay( points=self.mesh_grid_xy, - query_points=self._source_plane_data_grid_over_sampled, - use_voronoi_areas=use_voronoi_areas, - areas_factor=areas_factor, + query_points=self.data_grid.over_sampled, + areas_factor=self.mesh.areas_factor, ) ) @@ -555,9 +467,8 @@ def delaunay(self) -> "scipy.spatial.Delaunay": points, simplices, mappings, split_points, splitted_mappings = ( scipy_delaunay( points_np=self.mesh_grid_xy, - query_points_np=self._source_plane_data_grid_over_sampled, - use_voronoi_areas=use_voronoi_areas, - areas_factor=areas_factor, + query_points_np=self.data_grid.over_sampled, + areas_factor=self.mesh.areas_factor, ) ) @@ -569,14 +480,14 @@ def delaunay(self) -> "scipy.spatial.Delaunay": points, simplices, mappings = jax_delaunay_matern( points=self.mesh_grid_xy, - query_points=self._source_plane_data_grid_over_sampled, + query_points=self.data_grid.over_sampled, ) else: points, simplices, mappings = scipy_delaunay_matern( points_np=self.mesh_grid_xy, - query_points_np=self._source_plane_data_grid_over_sampled, + query_points_np=self.data_grid.over_sampled, ) split_points = None @@ -611,85 +522,90 @@ def split_points(self) -> np.ndarray: return self.delaunay.split_points @cached_property - def neighbors(self) -> Neighbors: + def _mappings_sizes_weights(self): """ - Returns a ndarray describing the neighbors of every pixel in a Delaunay triangulation, where a neighbor is - defined as two Delaunay triangles which are directly connected to one another in the triangulation. + Computes the following three quantities describing the mappings between of every sub-pixel in the masked data + and pixel in the `Delaunay` pixelization. - see `Neighbors` for a complete description of the neighboring scheme. + - `pix_indexes_for_sub_slim_index`: the mapping of every data pixel (given its `sub_slim_index`) + to pixelization pixels (given their `pix_indexes`). - The neighbors of a Voronoi mesh are computed using the `ridge_points` attribute of the scipy `Voronoi` - object, as described in the method `mesh_util.voronoi_neighbors_from`. - """ + - `pix_sizes_for_sub_slim_index`: the number of mappings of every data pixel to pixelization pixels. - delaunay = scipy.spatial.Delaunay(self.mesh_grid_xy) + - `pix_weights_for_sub_slim_index`: the interpolation weights of every data pixel's pixelization + pixel mapping - indptr, indices = delaunay.vertex_neighbor_vertices + These are packaged into the attributes `mappings`, `sizes` and `weights`. - sizes = indptr[1:] - indptr[:-1] + The `sub_slim_index` refers to the masked data sub-pixels and `pix_indexes` the pixelization pixel indexes, + for example: - neighbors = -1 * np.ones( - shape=(self.mesh_grid_xy.shape[0], int(np.max(sizes))), dtype="int" - ) + - `pix_indexes_for_sub_slim_index[0, 0] = 2`: The data's first (index 0) sub-pixel maps to the RectangularAdaptDensity + pixelization's third (index 2) pixel. - for k in range(self.mesh_grid_xy.shape[0]): - neighbors[k][0 : sizes[k]] = indices[indptr[k] : indptr[k + 1]] + - `pix_indexes_for_sub_slim_index[2, 0] = 4`: The data's third (index 2) sub-pixel maps to the RectangularAdaptDensity + pixelization's fifth (index 4) pixel. - return Neighbors(arr=neighbors.astype("int"), sizes=sizes.astype("int")) + The second dimension of the array `pix_indexes_for_sub_slim_index`, which is 0 in both examples above, is used + for cases where a data pixel maps to more than one pixelization pixel. - @cached_property - def voronoi(self) -> "scipy.spatial.Voronoi": - """ - Returns a `scipy.spatial.Voronoi` object from the 2D (y,x) grid of irregular coordinates, which correspond to - the centre of every Voronoi pixel. + For a `Delaunay` pixelization each data pixel maps to 3 Delaunay triangles with interpolation, for example: - This object contains numerous attributes describing a Voronoi mesh. PyAutoArray uses - the `vertex_neighbor_vertices` attribute to determine the neighbors of every Delaunay triangle. + - `pix_indexes_for_sub_slim_index[0, 0] = 2`: The data's first (index 0) sub-pixel maps to the Delaunay + pixelization's third (index 2) pixel. - There are numerous exceptions that `scipy.spatial.Delaunay` may raise when the input grid of coordinates used - to compute the Delaunay triangulation are ill posed. These exceptions are caught and combined into a single - `MeshException`, which helps exception handling in the `inversion` package. - """ - import scipy.spatial - from scipy.spatial import QhullError + - `pix_indexes_for_sub_slim_index[0, 1] = 5`: The data's first (index 0) sub-pixel also maps to the Delaunay + pixelization's sixth (index 5) pixel. - try: - return scipy.spatial.Voronoi( - self.mesh_grid_xy, - qhull_options="Qbb Qc Qx Qm", - ) - except (ValueError, OverflowError, QhullError) as e: - raise exc.MeshException() from e + - `pix_indexes_for_sub_slim_index[0, 2] = 8`: The data's first (index 0) sub-pixel also maps to the Delaunay + pixelization's ninth (index 8) pixel. - @property - def voronoi_areas(self): - return voronoi_areas_numpy(points=self.mesh_grid_xy) - - @property - def areas_for_magnification(self) -> np.ndarray: - """ - Returns the area of every Voronoi pixel in the Voronoi mesh. + The interpolation weights of these multiple mappings are stored in the array `pix_weights_for_sub_slim_index`. - Pixels at boundaries can sometimes have large unrealistic areas, which can impact the magnification - calculation. This method therefore sets their areas to zero so they do not impact the magnification - calculation. + For the Delaunay pixelization these mappings are calculated using the Scipy spatial library + (see `mapper_numba_util.pix_indexes_for_sub_slim_index_delaunay_from`). """ - areas = self.voronoi_areas + mappings = self.delaunay.mappings.astype("int") + sizes = self.delaunay.sizes.astype("int") - areas[areas == -1] = 0.0 + weights = pixel_weights_delaunay_from( + data_grid=self.data_grid.over_sampled, + mesh_grid=self.mesh_grid.array, + pix_indexes_for_sub_slim_index=mappings, + xp=self._xp, + ) - return areas + return mappings, sizes, weights - @property - def origin(self) -> Tuple[float, float]: - """ - The (y,x) origin of the Voronoi grid, which is fixed to (0.0, 0.0) for simplicity. + @cached_property + def _mappings_sizes_weights_split(self): """ - return 0.0, 0.0 + The property `_mappings_sizes_weightss` property describes the calculation of the mapping attributes, which contains + numpy arrays describing how data-points and mapper pixels map to one another and the weights of these mappings. - @property - def pixels(self) -> int: - """ - The total number of pixels in the Voronoi mesh. + For certain regularization schemes (e.g. `ConstantSplit`, `AdaptSplit`) regularization uses + mappings which are split in a cross configuration in order to factor in the derivative of the mapper + reconstruction. + + This property returns a unique set of mapping values used for these regularization schemes which compute + mappings and weights at each point on the split cross. """ - return self.shape[0] + splitted_weights = pixel_weights_delaunay_from( + data_grid=self.delaunay.split_points, + mesh_grid=self.mesh_grid.array, + pix_indexes_for_sub_slim_index=self.delaunay.splitted_mappings.astype( + "int" + ), + xp=self._xp, + ) + + append_line_int = np.zeros((len(splitted_weights), 1), dtype="int") - 1 + append_line_float = np.zeros((len(splitted_weights), 1), dtype="float") + + mappings = self._xp.hstack( + (self.delaunay.splitted_mappings.astype(self._xp.int32), append_line_int) + ) + sizes = self.delaunay.splitted_sizes.astype(self._xp.int32) + weights = self._xp.hstack((splitted_weights, append_line_float)) + + return mappings, sizes, weights diff --git a/autoarray/inversion/mesh/interpolator/knn.py b/autoarray/inversion/mesh/interpolator/knn.py new file mode 100644 index 000000000..584a96cc9 --- /dev/null +++ b/autoarray/inversion/mesh/interpolator/knn.py @@ -0,0 +1,241 @@ +from autoconf import cached_property + +from autoarray.inversion.mesh.interpolator.delaunay import InterpolatorDelaunay + + +def wendland_c4(r, h): + + import jax.numpy as jnp + + """ + Wendland C4: (1 - r/h)^6 * (35*(r/h)^2 + 18*r/h + 3) + C4 continuous, smoother, compact support + """ + s = r / (h + 1e-10) + w = jnp.where(s < 1.0, (1.0 - s) ** 6 * (35.0 * s**2 + 18.0 * s + 3.0), 0.0) + return w + + +def get_interpolation_weights( + points, query_points, k_neighbors, radius_scale, point_block=128 +): + import jax + import jax.numpy as jnp + + points = jnp.asarray(points) + query_points = jnp.asarray(query_points) + + M = int(query_points.shape[0]) + N = int(points.shape[0]) + + if N == 0: + raise ValueError("points has zero length; cannot compute kNN weights.") + + # Clamp k so top_k is valid even when N < k_neighbors + k = int(k_neighbors) + if k > N: + k = N + + # Clamp block so dynamic_slice is always valid even when N < point_block + B = int(point_block) + if B > N: + B = N + + # Precompute ||q||^2 once (M, 1) + q2 = jnp.sum(query_points * query_points, axis=1, keepdims=True) + + # Running best: store NEGATIVE squared distances so we can use lax.top_k (largest) + best_vals = -jnp.inf * jnp.ones((M, k), dtype=query_points.dtype) + best_idx = jnp.zeros((M, k), dtype=jnp.int32) + + # How many blocks + n_blocks = (N + B - 1) // B + + def body_fun(bi, carry): + best_vals, best_idx = carry + start = bi * B + + # How many valid points in this block (<= B) + block_n = jnp.minimum(B, N - start) + + # Safe because B <= N by construction + p_block = jax.lax.dynamic_slice(points, (start, 0), (B, points.shape[1])) + + # Mask out padded rows (only matters for last block) + mask = jnp.arange(B) < block_n # (B,) + + # dist_sq = ||q||^2 + ||p||^2 - 2 q·p + p2 = jnp.sum(p_block * p_block, axis=1, keepdims=True).T # (1, B) + qp = query_points @ p_block.T # (M, B) + dist_sq = q2 + p2 - 2.0 * qp # (M, B) + dist_sq = jnp.maximum(dist_sq, 0.0) + + # Invalidate padded points + dist_sq = jnp.where(mask[None, :], dist_sq, jnp.inf) + + vals = -dist_sq # (M, B) + + # Indices for this block (M, B) + idx_block = (start + jnp.arange(B, dtype=jnp.int32))[None, :] + idx_block = jnp.broadcast_to(idx_block, (M, B)) + + # Merge + top-k + merged_vals = jnp.concatenate([best_vals, vals], axis=1) # (M, k+B) + merged_idx = jnp.concatenate([best_idx, idx_block], axis=1) + + new_vals, new_pos = jax.lax.top_k(merged_vals, k) + new_idx = jnp.take_along_axis(merged_idx, new_pos, axis=1) + + return new_vals, new_idx + + best_vals, best_idx = jax.lax.fori_loop( + 0, n_blocks, body_fun, (best_vals, best_idx) + ) + + # Distances for selected k + knn_dist_sq = -best_vals + knn_distances = jnp.sqrt(knn_dist_sq + 1e-20) + + # Radius per query + h = jnp.max(knn_distances, axis=1, keepdims=True) * radius_scale + + # Wendland weights + normalize + weights = wendland_c4(knn_distances, h) + weights_sum = jnp.sum(weights, axis=1, keepdims=True) + 1e-10 + weights_normalized = weights / weights_sum + + return best_idx, weights_normalized, knn_distances + + +def kernel_interpolate_points(points, query_chunk, values, k, radius_scale): + """ + Compute kernel interpolation for a chunk of query points using K nearest neighbors. + + Args: + query_chunk: (M, 2) query points + points: (N, 2) source points + values: (N,) values at source points + k: number of nearest neighbors + radius_scale: multiplier for radius + + Returns: + (M,) interpolated values + """ + + import jax.numpy as jnp + + # Compute weights using the intermediate function + top_k_indices, weights_normalized, _ = get_interpolation_weights( + points, + query_chunk, + k, + radius_scale, + ) + + # Get neighbor values + neighbor_values = values[top_k_indices] # (M, k) + + # Interpolate: weighted sum + interpolated = jnp.sum(weights_normalized * neighbor_values, axis=1) # (M,) + + return interpolated + + +class InterpolatorKNearestNeighbor(InterpolatorDelaunay): + + @cached_property + def _mappings_sizes_weights(self): + + try: + query_points = self.data_grid.over_sampled + except AttributeError: + query_points = self.data_grid + + mappings, weights, _ = get_interpolation_weights( + points=self.mesh_grid_xy, + query_points=query_points, + k_neighbors=self.mesh.k_neighbors, + radius_scale=self.mesh.radius_scale, + ) + + mappings = self._xp.asarray(mappings) + weights = self._xp.asarray(weights) + + sizes = self._xp.full( + (mappings.shape[0],), + mappings.shape[1], + ) + + return mappings, sizes, weights + + @cached_property + def distance_to_self(self): + + _, _, distance_to_self = get_interpolation_weights( + points=self.mesh_grid_xy, + query_points=self.mesh_grid_xy, + k_neighbors=self.mesh.k_neighbors, + radius_scale=self.mesh.radius_scale, + ) + + return distance_to_self + + @cached_property + def _mappings_sizes_weights_split(self): + """ + kNN mappings + kernel weights computed at split points (for split regularization schemes), + with split-point step sizes derived from kNN local spacing (no Delaunay / simplices). + """ + from autoarray.inversion.regularization.regularization_util import ( + split_points_from, + ) + + neighbor_index = int(self.mesh.k_neighbors) // self.mesh.split_neighbor_division + # e.g. k=10, division=2 -> neighbor_index=5 + + distance_to_self = self.distance_to_self # (N, k_neighbors), col 0 is self + + others = distance_to_self[:, 1:] # (N, k_neighbors-1) + + # Clamp to valid range (0-based indexing into `others`) + idx = int(neighbor_index) - 1 + idx = max(0, min(idx, others.shape[1] - 1)) + + r_k = others[:, idx] # (N,) + + # Split cross step size (length): sqrt(area) ~ r_k + split_step = self.mesh.areas_factor * r_k # (N,) + + # Split points (xp-native) + split_points = split_points_from( + points=self.mesh_grid.array, + area_weights=split_step, + xp=self._xp, + ) + + interpolator = InterpolatorKNearestNeighbor( + mesh=self.mesh, + mesh_grid=self.mesh_grid, + data_grid=split_points, + preloads=self.preloads, + xp=self._xp, + ) + + mappings = interpolator.mappings + weights = interpolator.weights + + sizes = self._xp.full( + (mappings.shape[0],), + mappings.shape[1], + ) + + return mappings, sizes, weights + + # def interpolate(self, query_points, points, values): + # return kernel_interpolate_points( + # points=self.mesh_grid_xy, + # query_points=self.data_grid.over_sampled, + # values, + # k=self.mesh.k_neighbors, + # radius_scale=self.mesh.radius_scale, + # ) diff --git a/autoarray/inversion/mesh/interpolator/rectangular.py b/autoarray/inversion/mesh/interpolator/rectangular.py new file mode 100644 index 000000000..2462bfbe6 --- /dev/null +++ b/autoarray/inversion/mesh/interpolator/rectangular.py @@ -0,0 +1,309 @@ +import numpy as np +from functools import partial + +from autoconf import cached_property + +from autoarray.inversion.mesh.interpolator.abstract import AbstractInterpolator + + +def forward_interp(xp, yp, x): + + import jax + import jax.numpy as jnp + + return jax.vmap(jnp.interp, in_axes=(1, 1, 1, None, None), out_axes=(1))( + x, xp, yp, 0, 1 + ) + + +def reverse_interp(xp, yp, x): + import jax + import jax.numpy as jnp + + return jax.vmap(jnp.interp, in_axes=(1, 1, 1), out_axes=(1))(x, xp, yp) + + +def forward_interp_np(xp, yp, x): + """ + xp: (N, M) + yp: (N, M) + x : (M,) ← one x per column + """ + + if yp.ndim == 1 and xp.ndim == 2: + yp = np.broadcast_to(yp[:, None], xp.shape) + + K, M = x.shape + + out = np.empty((K, 2), dtype=xp.dtype) + + for j in range(2): + out[:, j] = np.interp(x[:, j], xp[:, j], yp[:, j], left=0, right=1) + + return out + + +def reverse_interp_np(xp, yp, x): + """ + xp : (N,) or (N, M) + yp : (N, M) + x : (K, M) query points per column + """ + + # Ensure xp is 2D: (N, M) + if xp.ndim == 1 and yp.ndim == 2: # (N, 1) + xp = np.broadcast_to(xp[:, None], yp.shape) + + # Shapes + K, M = x.shape + + # Output + out = np.empty((K, 2), dtype=yp.dtype) + + # Column-wise interpolation (cannot avoid this loop in pure NumPy) + for j in range(2): + out[:, j] = np.interp(x[:, j], xp[:, j], yp[:, j]) + + return out + + +def create_transforms(traced_points, mesh_weight_map=None, xp=np): + + N = traced_points.shape[0] # // 2 + + if mesh_weight_map is None: + t = xp.arange(1, N + 1) / (N + 1) + t = xp.stack([t, t], axis=1) + sort_points = xp.sort(traced_points, axis=0) # [::2] + else: + sdx = xp.argsort(traced_points, axis=0) + sort_points = xp.take_along_axis(traced_points, sdx, axis=0) + t = xp.stack([mesh_weight_map, mesh_weight_map], axis=1) + t = xp.take_along_axis(t, sdx, axis=0) + t = xp.cumsum(t, axis=0) + + if xp.__name__.startswith("jax"): + transform = partial(forward_interp, sort_points, t) + inv_transform = partial(reverse_interp, t, sort_points) + return transform, inv_transform + + transform = partial(forward_interp_np, sort_points, t) + inv_transform = partial(reverse_interp_np, t, sort_points) + return transform, inv_transform + + +def adaptive_rectangular_transformed_grid_from( + data_grid, grid, mesh_weight_map=None, xp=np +): + + mu = data_grid.mean(axis=0) + scale = data_grid.std(axis=0).min() + source_grid_scaled = (data_grid - mu) / scale + + transform, inv_transform = create_transforms( + source_grid_scaled, mesh_weight_map=mesh_weight_map, xp=xp + ) + + def inv_full(U): + return inv_transform(U) * scale + mu + + return inv_full(grid) + + +def adaptive_rectangular_mappings_weights_via_interpolation_from( + source_grid_size: int, + data_grid, + data_grid_over_sampled, + mesh_weight_map=None, + xp=np, +): + """ + Compute bilinear interpolation indices and weights for mapping an oversampled + source-plane grid onto a regular rectangular pixelization. + + This function takes a set of irregularly-sampled source-plane coordinates and + builds an adaptive mapping onto a `source_grid_size x source_grid_size` rectangular + pixelization using bilinear interpolation. The interpolation is expressed as: + + f(x, y) ≈ w_bl * f(ix_down, iy_down) + + w_br * f(ix_up, iy_down) + + w_tl * f(ix_down, iy_up) + + w_tr * f(ix_up, iy_up) + + where `(ix_down, ix_up, iy_down, iy_up)` are the integer grid coordinates + surrounding the continuous position `(x, y)`. + + Steps performed: + 1. Normalize the source-plane grid by subtracting its mean and dividing by + the minimum axis standard deviation (to balance scaling). + 2. Construct forward/inverse transforms which map the grid into the unit square [0,1]^2. + 3. Transform the oversampled source-plane grid into [0,1]^2, then scale it + to index space `[0, source_grid_size)`. + 4. Compute floor/ceil along x and y axes to find the enclosing rectangular cell. + 5. Build the four corner indices: bottom-left (bl), bottom-right (br), + top-left (tl), and top-right (tr). + 6. Flatten the 2D indices into 1D indices suitable for scatter operations, + with a flipped row-major convention: row = source_grid_size - i, col = j. + 7. Compute bilinear interpolation weights (`w_bl, w_br, w_tl, w_tr`). + 8. Return arrays of flattened indices and weights of shape `(N, 4)`, where + `N` is the number of oversampled coordinates. + + Parameters + ---------- + source_grid_size : int + The number of pixels along one dimension of the rectangular pixelization. + The grid is square: (source_grid_size x source_grid_size). + data_grid : (M, 2) ndarray + The base source-plane coordinates, used to define normalization and transforms. + data_grid_over_sampled : (N, 2) ndarray + Oversampled source-plane coordinates to be interpolated onto the rectangular grid. + mesh_weight_map + The weight map used to weight the creation of the rectangular mesh grid, which is used for the + `RectangularBrightness` mesh which adapts the size of its pixels to where the source is reconstructed. + + Returns + ------- + flat_indices : (N, 4) int ndarray + The flattened indices of the four neighboring pixel corners for each oversampled point. + Order: [bl, br, tl, tr]. + weights : (N, 4) float ndarray + The bilinear interpolation weights for each of the four neighboring pixels. + Order: [w_bl, w_br, w_tl, w_tr]. + """ + + # --- Step 1. Normalize grid --- + mu = data_grid.mean(axis=0) + scale = data_grid.std(axis=0).min() + source_grid_scaled = (data_grid - mu) / scale + + # --- Step 2. Build transforms --- + transform, inv_transform = create_transforms( + source_grid_scaled, mesh_weight_map=mesh_weight_map, xp=xp + ) + + # --- Step 3. Transform oversampled grid into index space --- + grid_over_sampled_scaled = (data_grid_over_sampled - mu) / scale + grid_over_sampled_transformed = transform(grid_over_sampled_scaled) + grid_over_index = (source_grid_size - 3) * grid_over_sampled_transformed + 1 + + # --- Step 4. Floor/ceil indices --- + ix_down = xp.floor(grid_over_index[:, 0]) + ix_up = xp.ceil(grid_over_index[:, 0]) + iy_down = xp.floor(grid_over_index[:, 1]) + iy_up = xp.ceil(grid_over_index[:, 1]) + + # --- Step 5. Four corners --- + idx_tl = xp.stack([ix_up, iy_down], axis=1) + idx_tr = xp.stack([ix_up, iy_up], axis=1) + idx_br = xp.stack([ix_down, iy_up], axis=1) + idx_bl = xp.stack([ix_down, iy_down], axis=1) + + # --- Step 6. Flatten indices --- + def flatten(idx, n): + row = n - idx[:, 0] + col = idx[:, 1] + return row * n + col + + flat_tl = flatten(idx_tl, source_grid_size) + flat_tr = flatten(idx_tr, source_grid_size) + flat_bl = flatten(idx_bl, source_grid_size) + flat_br = flatten(idx_br, source_grid_size) + + flat_indices = xp.stack([flat_tl, flat_tr, flat_bl, flat_br], axis=1).astype( + "int64" + ) + + # --- Step 7. Bilinear interpolation weights --- + t_row = (grid_over_index[:, 0] - ix_down) / (ix_up - ix_down + 1e-12) + t_col = (grid_over_index[:, 1] - iy_down) / (iy_up - iy_down + 1e-12) + + # Weights + w_tl = (1 - t_row) * (1 - t_col) + w_tr = (1 - t_row) * t_col + w_bl = t_row * (1 - t_col) + w_br = t_row * t_col + weights = xp.stack([w_tl, w_tr, w_bl, w_br], axis=1) + + return flat_indices, weights + + +class InterpolatorRectangular(AbstractInterpolator): + + def __init__( + self, + mesh, + mesh_grid, + data_grid, + mesh_weight_map, + adapt_data: np.ndarray = None, + preloads=None, + xp=np, + ): + """ + A grid of (y,x) coordinates which represent a uniform rectangular pixelization. + + A `InterpolatorRectangular` is ordered such pixels begin from the top-row and go rightwards and then downwards. + It is an ndarray of shape [total_pixels, 2], where the first dimension of the ndarray corresponds to the + pixelization's pixel index and second element whether it is a y or x arc-second coordinate. + + For example: + + - grid[3,0] = the y-coordinate of the 4th pixel in the rectangular pixelization. + - grid[6,1] = the x-coordinate of the 7th pixel in the rectangular pixelization. + + This class is used in conjuction with the `inversion/pixelizations` package to create rectangular pixelizations + and mappers that perform an `Inversion`. + + Parameters + ---------- + values + The grid of (y,x) coordinates corresponding to the centres of each pixel in the rectangular pixelization. + shape_native + The 2D dimensions of the rectangular pixelization with shape (y_pixels, x_pixel). + pixel_scales + The (y,x) scaled units to pixel units conversion factors of every pixel. If this is input as a `float`, + it is converted to a (float, float) structure. + origin + The (y,x) origin of the pixelization. + """ + super().__init__( + mesh=mesh, + mesh_grid=mesh_grid, + data_grid=data_grid, + adapt_data=adapt_data, + preloads=preloads, + xp=xp, + ) + self.mesh_weight_map = mesh_weight_map + + @cached_property + def mesh_geometry(self): + + from autoarray.inversion.mesh.mesh_geometry.rectangular import ( + MeshGeometryRectangular, + ) + + return MeshGeometryRectangular( + mesh=self.mesh, + mesh_grid=self.mesh_grid, + data_grid=self.data_grid, + mesh_weight_map=self.mesh_weight_map, + xp=self._xp, + ) + + @cached_property + def _mappings_sizes_weights(self): + + mappings, weights = ( + adaptive_rectangular_mappings_weights_via_interpolation_from( + source_grid_size=self.mesh.shape[0], + data_grid=self.data_grid.array, + data_grid_over_sampled=self.data_grid.over_sampled, + mesh_weight_map=self.mesh_weight_map, + xp=self._xp, + ) + ) + + sizes = 4 * self._xp.ones(len(mappings), dtype="int") + + return mappings, sizes, weights diff --git a/autoarray/inversion/mesh/interpolator/rectangular_uniform.py b/autoarray/inversion/mesh/interpolator/rectangular_uniform.py new file mode 100644 index 000000000..d3c7f7a3c --- /dev/null +++ b/autoarray/inversion/mesh/interpolator/rectangular_uniform.py @@ -0,0 +1,178 @@ +from typing import Tuple +import numpy as np + +from autoconf import cached_property + +from autoarray.inversion.mesh.interpolator.abstract import AbstractInterpolator +from autoarray.geometry.geometry_2d import Geometry2D + + +def rectangular_mappings_weights_via_interpolation_from( + shape_native: Tuple[int, int], + data_grid: np.ndarray, + mesh_grid: np.ndarray, + xp=np, +): + """ + Compute bilinear interpolation weights and corresponding rectangular mesh indices for an irregular grid. + + Given a flattened regular rectangular mesh grid and an irregular grid of data points, this function + determines for each irregular point: + - the indices of the 4 nearest rectangular mesh pixels (top-left, top-right, bottom-left, bottom-right), and + - the bilinear interpolation weights with respect to those pixels. + + The function supports JAX and is compatible with JIT compilation. + + Parameters + ---------- + shape_native + The shape (Ny, Nx) of the original rectangular mesh grid before flattening. + data_grid + The irregular grid of (y, x) points to interpolate. + mesh_grid + The flattened regular rectangular mesh grid of (y, x) coordinates. + + Returns + ------- + mappings : np.ndarray of shape (N, 4) + Indices of the four nearest rectangular mesh pixels in the flattened mesh grid. + Order is: top-left, top-right, bottom-left, bottom-right. + weights : np.ndarray of shape (N, 4) + Bilinear interpolation weights corresponding to the four nearest mesh pixels. + + Notes + ----- + - Assumes the mesh grid is uniformly spaced. + - The weights sum to 1 for each irregular point. + - Uses bilinear interpolation in the (y, x) coordinate system. + """ + mesh_grid = mesh_grid.reshape(*shape_native, 2) + + # Assume mesh is shaped (Ny, Nx, 2) + Ny, Nx = mesh_grid.shape[:2] + + # Get mesh spacings and lower corner + y_coords = mesh_grid[:, 0, 0] # shape (Ny,) + x_coords = mesh_grid[0, :, 1] # shape (Nx,) + + dy = y_coords[1] - y_coords[0] + dx = x_coords[1] - x_coords[0] + + y_min = y_coords[0] + x_min = x_coords[0] + + # shape (N_irregular, 2) + irregular = data_grid + + # Compute normalized mesh coordinates (floating indices) + fy = (irregular[:, 0] - y_min) / dy + fx = (irregular[:, 1] - x_min) / dx + + # Integer indices of top-left corners + ix = xp.floor(fx).astype(xp.int32) + iy = xp.floor(fy).astype(xp.int32) + + # Clip to stay within bounds + ix = xp.clip(ix, 0, Nx - 2) + iy = xp.clip(iy, 0, Ny - 2) + + # Local coordinates inside the cell (0 <= tx, ty <= 1) + tx = fx - ix + ty = fy - iy + + # Bilinear weights + w00 = (1 - tx) * (1 - ty) + w10 = tx * (1 - ty) + w01 = (1 - tx) * ty + w11 = tx * ty + + weights = xp.stack([w00, w10, w01, w11], axis=1) # shape (N_irregular, 4) + + # Compute indices of 4 surrounding pixels in the flattened mesh + i00 = iy * Nx + ix + i10 = iy * Nx + (ix + 1) + i01 = (iy + 1) * Nx + ix + i11 = (iy + 1) * Nx + (ix + 1) + + mappings = xp.stack([i00, i10, i01, i11], axis=1) # shape (N_irregular, 4) + + return mappings, weights + + +class InterpolatorRectangularUniform(AbstractInterpolator): + + def __init__( + self, + mesh, + mesh_grid, + data_grid, + adapt_data: np.ndarray = None, + mesh_weight_map: np.ndarray = None, + preloads=None, + xp=np, + ): + """ + A grid of (y,x) coordinates which represent a uniform rectangular pixelization. + + A `InterpolatorRectangular` is ordered such pixels begin from the top-row and go rightwards and then downwards. + It is an ndarray of shape [total_pixels, 2], where the first dimension of the ndarray corresponds to the + pixelization's pixel index and second element whether it is a y or x arc-second coordinate. + + For example: + + - grid[3,0] = the y-coordinate of the 4th pixel in the rectangular pixelization. + - grid[6,1] = the x-coordinate of the 7th pixel in the rectangular pixelization. + + This class is used in conjuction with the `inversion/pixelizations` package to create rectangular pixelizations + and mappers that perform an `Inversion`. + + Parameters + ---------- + values + The grid of (y,x) coordinates corresponding to the centres of each pixel in the rectangular pixelization. + shape_native + The 2D dimensions of the rectangular pixelization with shape (y_pixels, x_pixel). + pixel_scales + The (y,x) scaled units to pixel units conversion factors of every pixel. If this is input as a `float`, + it is converted to a (float, float) structure. + origin + The (y,x) origin of the pixelization. + """ + super().__init__( + mesh=mesh, + mesh_grid=mesh_grid, + data_grid=data_grid, + adapt_data=adapt_data, + preloads=preloads, + xp=xp, + ) + self.mesh_weight_map = mesh_weight_map + + @cached_property + def mesh_geometry(self): + + from autoarray.inversion.mesh.mesh_geometry.rectangular import ( + MeshGeometryRectangular, + ) + + return MeshGeometryRectangular( + mesh=self.mesh, + mesh_grid=self.mesh_grid, + data_grid=self.data_grid, + mesh_weight_map=self.mesh_weight_map, + xp=self._xp, + ) + + @cached_property + def _mappings_sizes_weights(self): + + mappings, weights = rectangular_mappings_weights_via_interpolation_from( + shape_native=self.mesh.shape, + mesh_grid=self.mesh_grid.array, + data_grid=self.data_grid.over_sampled, + xp=self._xp, + ) + + sizes = 4 * self._xp.ones(len(mappings), dtype="int") + + return mappings, sizes, weights diff --git a/autoarray/inversion/mesh/mesh/__init__.py b/autoarray/inversion/mesh/mesh/__init__.py new file mode 100644 index 000000000..34c1cb24d --- /dev/null +++ b/autoarray/inversion/mesh/mesh/__init__.py @@ -0,0 +1,6 @@ +from .abstract import AbstractMesh as Mesh +from .rectangular_adapt_density import RectangularAdaptDensity +from .rectangular_adapt_image import RectangularAdaptImage +from .rectangular_uniform import RectangularUniform +from .delaunay import Delaunay +from .knn import KNearestNeighbor diff --git a/autoarray/inversion/pixelization/mesh/abstract.py b/autoarray/inversion/mesh/mesh/abstract.py similarity index 89% rename from autoarray/inversion/pixelization/mesh/abstract.py rename to autoarray/inversion/mesh/mesh/abstract.py index 314b98bcb..8bacf78b4 100644 --- a/autoarray/inversion/pixelization/mesh/abstract.py +++ b/autoarray/inversion/mesh/mesh/abstract.py @@ -1,8 +1,9 @@ import numpy as np from typing import Optional -from autoarray.inversion.pixelization.mappers.mapper_grids import MapperGrids -from autoarray.inversion.pixelization.border_relocator import BorderRelocator +from autoarray.settings import Settings +from autoarray.inversion.mesh.border_relocator import BorderRelocator +from autoarray.inversion.regularization.abstract import AbstractRegularization from autoarray.structures.grids.uniform_2d import Grid2D from autoarray.structures.grids.irregular_2d import Grid2DIrregular @@ -92,22 +93,13 @@ def relocated_mesh_grid_from( ) return source_plane_mesh_grid - def mapper_grids_from( + def interpolator_from( self, - mask, source_plane_data_grid: Grid2D, + source_plane_mesh_grid: Grid2DIrregular, border_relocator: Optional[BorderRelocator] = None, - source_plane_mesh_grid: Optional[Grid2DIrregular] = None, - image_plane_mesh_grid: Optional[Grid2DIrregular] = None, adapt_data: np.ndarray = None, - xp=np, - ) -> MapperGrids: - raise NotImplementedError - - def mesh_grid_from( - self, - source_plane_data_grid: Grid2D, - source_plane_mesh_grid: Grid2DIrregular, + preloads=None, xp=np, ): raise NotImplementedError diff --git a/autoarray/inversion/pixelization/mesh/delaunay.py b/autoarray/inversion/mesh/mesh/delaunay.py similarity index 67% rename from autoarray/inversion/pixelization/mesh/delaunay.py rename to autoarray/inversion/mesh/mesh/delaunay.py index 8bcc4ee5c..8da4bb671 100644 --- a/autoarray/inversion/pixelization/mesh/delaunay.py +++ b/autoarray/inversion/mesh/mesh/delaunay.py @@ -1,16 +1,14 @@ import numpy as np from typing import Optional -from autoarray.inversion.pixelization.mappers.mapper_grids import MapperGrids -from autoarray.inversion.pixelization.border_relocator import BorderRelocator -from autoarray.inversion.pixelization.mesh.abstract import AbstractMesh -from autoarray.structures.mesh.delaunay_2d import Mesh2DDelaunay +from autoarray.inversion.mesh.border_relocator import BorderRelocator +from autoarray.inversion.mesh.mesh.abstract import AbstractMesh from autoarray.structures.grids.uniform_2d import Grid2D from autoarray.structures.grids.irregular_2d import Grid2DIrregular class Delaunay(AbstractMesh): - def __init__(self): + def __init__(self, areas_factor: float = 0.5): """ An irregular mesh of Delaunay triangle pixels, which using linear barycentric interpolation are paired with a 2D grid of (y,x) coordinates. @@ -35,49 +33,36 @@ def __init__(self): triangle corners they are a closer distance to. """ super().__init__() + self.areas_factor = areas_factor - def mesh_grid_from( - self, - source_plane_data_grid=None, - source_plane_mesh_grid=None, - preloads=None, - xp=np, - ): + @property + def skip_areas(self): """ - Return the Delaunay ``source_plane_mesh_grid`` as a ``Mesh2DDelaunay`` object, which provides additional - functionality for performing operations that exploit the geometry of a Delaunay mesh. - - Parameters - ---------- - source_plane_data_grid - A 2D grid of (y,x) coordinates associated with the unmasked 2D data after it has been transformed to the - ``source`` reference frame. - source_plane_mesh_grid - The centres of every Delaunay pixel in the ``source`` frame, which are initially derived by computing a sparse - set of (y,x) coordinates computed from the unmasked data in the image-plane and applying a transformation - to this. - settings - Settings controlling the pixelization for example if a border is used to relocate its exterior coordinates. + Whether to skip barycentric area calculations and split point computations during Delaunay triangulation. + When True, the Delaunay interface returns only the minimal set of outputs (points, simplices, mappings) + without computing split_points or splitted_mappings. This optimization is useful for regularization + schemes like Matérn kernels that don't require area-based calculations. Default is False. """ + return False - return Mesh2DDelaunay( - values=source_plane_mesh_grid, - source_plane_data_grid_over_sampled=source_plane_data_grid, - preloads=preloads, - _xp=xp, + @property + def interpolator_cls(self): + + from autoarray.inversion.mesh.interpolator.delaunay import ( + InterpolatorDelaunay, ) - def mapper_grids_from( + return InterpolatorDelaunay + + def interpolator_from( self, - mask, source_plane_data_grid: Grid2D, + source_plane_mesh_grid: Grid2DIrregular, border_relocator: Optional[BorderRelocator] = None, - source_plane_mesh_grid: Optional[Grid2DIrregular] = None, - image_plane_mesh_grid: Optional[Grid2DIrregular] = None, adapt_data: np.ndarray = None, preloads=None, xp=np, - ) -> MapperGrids: + ): """ Mapper objects describe the mappings between pixels in the masked 2D data and the pixels in a mesh, in both the `data` and `source` frames. @@ -117,7 +102,6 @@ def mapper_grids_from( adapt_data Not used for a rectangular mesh. """ - relocated_grid = self.relocated_grid_from( border_relocator=border_relocator, source_plane_data_grid=source_plane_data_grid, @@ -131,17 +115,11 @@ def mapper_grids_from( xp=xp, ) - source_plane_mesh_grid = self.mesh_grid_from( - source_plane_data_grid=relocated_grid.over_sampled, - source_plane_mesh_grid=relocated_mesh_grid, + return self.interpolator_cls( + mesh=self, + data_grid=relocated_grid, + mesh_grid=relocated_mesh_grid, + adapt_data=adapt_data, preloads=preloads, xp=xp, ) - - return MapperGrids( - mask=mask, - source_plane_data_grid=relocated_grid, - source_plane_mesh_grid=source_plane_mesh_grid, - image_plane_mesh_grid=image_plane_mesh_grid, - adapt_data=adapt_data, - ) diff --git a/autoarray/inversion/mesh/mesh/knn.py b/autoarray/inversion/mesh/mesh/knn.py new file mode 100644 index 000000000..66dcdee24 --- /dev/null +++ b/autoarray/inversion/mesh/mesh/knn.py @@ -0,0 +1,38 @@ +from autoarray.inversion.mesh.mesh.delaunay import Delaunay + + +class KNearestNeighbor(Delaunay): + + def __init__( + self, + k_neighbors=10, + radius_scale=1.5, + areas_factor=0.5, + split_neighbor_division=2, + ): + + self.k_neighbors = k_neighbors + self.radius_scale = radius_scale + self.areas_factor = areas_factor + self.split_neighbor_division = split_neighbor_division + + super().__init__() + + @property + def skip_areas(self): + """ + Whether to skip barycentric area calculations and split point computations during Delaunay triangulation. + When True, the Delaunay interface returns only the minimal set of outputs (points, simplices, mappings) + without computing split_points or splitted_mappings. This optimization is useful for regularization + schemes like Matérn kernels that don't require area-based calculations. Default is False. + """ + return False + + @property + def interpolator_cls(self): + + from autoarray.inversion.mesh.interpolator.knn import ( + InterpolatorKNearestNeighbor, + ) + + return InterpolatorKNearestNeighbor diff --git a/autoarray/inversion/pixelization/mesh/rectangular.py b/autoarray/inversion/mesh/mesh/rectangular_adapt_density.py similarity index 51% rename from autoarray/inversion/pixelization/mesh/rectangular.py rename to autoarray/inversion/mesh/mesh/rectangular_adapt_density.py index 5afad2148..61b0b1537 100644 --- a/autoarray/inversion/pixelization/mesh/rectangular.py +++ b/autoarray/inversion/mesh/mesh/rectangular_adapt_density.py @@ -1,18 +1,66 @@ import numpy as np from typing import Optional, Tuple - +from autoarray.settings import Settings +from autoarray.inversion.regularization.abstract import AbstractRegularization from autoarray.structures.grids.irregular_2d import Grid2DIrregular from autoarray.structures.grids.uniform_2d import Grid2D -from autoarray.structures.mesh.rectangular_2d import Mesh2DRectangular +from autoarray.inversion.mesh.mesh.abstract import AbstractMesh +from autoarray.inversion.mesh.border_relocator import BorderRelocator -from autoarray.inversion.pixelization.mappers.mapper_grids import MapperGrids -from autoarray.inversion.pixelization.mesh.abstract import AbstractMesh -from autoarray.inversion.pixelization.border_relocator import BorderRelocator +from autoarray.structures.grids import grid_2d_util from autoarray import exc +def overlay_grid_from( + shape_native: Tuple[int, int], + grid: np.ndarray, + buffer: float = 1e-8, + xp=np, +) -> np.ndarray: + """ + Creates a `Grid2DRecntagular` by overlaying the rectangular pixelization over an input grid of (y,x) + coordinates. + + This is performed by first computing the minimum and maximum y and x coordinates of the input grid. A + rectangular pixelization with dimensions `shape_native` is then laid over the grid using these coordinates, + such that the extreme edges of this rectangular pixelization overlap these maximum and minimum (y,x) coordinates. + + A a `buffer` can be included which increases the size of the rectangular pixelization, placing additional + spacing beyond these maximum and minimum coordinates. + + Parameters + ---------- + shape_native + The 2D dimensions of the rectangular pixelization with shape (y_pixels, x_pixel). + grid + A grid of (y,x) coordinates which the rectangular pixelization is laid-over. + buffer + The size of the extra spacing placed between the edges of the rectangular pixelization and input grid. + """ + grid = grid.array + + y_min = xp.min(grid[:, 0]) - buffer + y_max = xp.max(grid[:, 0]) + buffer + x_min = xp.min(grid[:, 1]) - buffer + x_max = xp.max(grid[:, 1]) + buffer + + pixel_scales = xp.array( + ( + (y_max - y_min) / shape_native[0], + (x_max - x_min) / shape_native[1], + ) + ) + origin = xp.array(((y_max + y_min) / 2.0, (x_max + x_min) / 2.0)) + + grid_slim = grid_2d_util.grid_2d_slim_via_shape_native_not_mask_from( + shape_native=shape_native, pixel_scales=pixel_scales, origin=origin, xp=xp + ) + + return grid_slim + + class RectangularAdaptDensity(AbstractMesh): def __init__(self, shape: Tuple[int, int] = (3, 3)): """ @@ -52,6 +100,14 @@ def __init__(self, shape: Tuple[int, int] = (3, 3)): self.pixels = self.shape[0] * self.shape[1] super().__init__() + @property + def interpolator_cls(self): + from autoarray.inversion.mesh.interpolator.rectangular import ( + InterpolatorRectangular, + ) + + return InterpolatorRectangular + def mesh_weight_map_from(self, adapt_data, xp=np) -> np.ndarray: """ The weight map of a rectangular pixelization is None, because magnificaiton adaption uses @@ -65,17 +121,15 @@ def mesh_weight_map_from(self, adapt_data, xp=np) -> np.ndarray: """ return None - def mapper_grids_from( + def interpolator_from( self, - mask, source_plane_data_grid: Grid2D, + source_plane_mesh_grid: Grid2DIrregular, border_relocator: Optional[BorderRelocator] = None, - source_plane_mesh_grid: Grid2D = None, - image_plane_mesh_grid: Grid2D = None, adapt_data: np.ndarray = None, preloads=None, xp=np, - ) -> MapperGrids: + ): """ Mapper objects describe the mappings between pixels in the masked 2D data and the pixels in a pixelization, in both the `data` and `source` frames. @@ -107,116 +161,26 @@ def mapper_grids_from( adapt_data Not used for a rectangular pixelization. """ - relocated_grid = self.relocated_grid_from( border_relocator=border_relocator, source_plane_data_grid=source_plane_data_grid, xp=xp, ) - mesh_grid = self.mesh_grid_from(source_plane_data_grid=relocated_grid, xp=xp) - - mesh_weight_map = self.mesh_weight_map_from(adapt_data=adapt_data, xp=xp) - - return MapperGrids( - mask=mask, - source_plane_data_grid=relocated_grid, - source_plane_mesh_grid=mesh_grid, - image_plane_mesh_grid=image_plane_mesh_grid, - adapt_data=adapt_data, - mesh_weight_map=mesh_weight_map, - ) - - def mesh_grid_from( - self, - source_plane_data_grid: Optional[Grid2D] = None, - source_plane_mesh_grid: Optional[Grid2D] = None, - xp=np, - ) -> Mesh2DRectangular: - """ - Return the rectangular `source_plane_mesh_grid` as a `Mesh2DRectangular` object, which provides additional - functionality for perform operatons that exploit the geometry of a rectangular pixelization. - - Parameters - ---------- - source_plane_data_grid - The (y,x) grid of coordinates over which the rectangular pixelization is overlaid, where this grid may have - had exterior pixels relocated to its edge via the border. - source_plane_mesh_grid - Not used for a rectangular pixelization, because the pixelization grid in the `source` frame is computed - by overlaying the `source_plane_data_grid` with the rectangular pixelization. - """ - return Mesh2DRectangular.overlay_grid( + mesh_grid = overlay_grid_from( shape_native=self.shape, - grid=Grid2DIrregular(source_plane_data_grid.over_sampled), + grid=Grid2DIrregular(relocated_grid.over_sampled), xp=xp, ) + mesh_weight_map = self.mesh_weight_map_from(adapt_data=adapt_data, xp=xp) -class RectangularAdaptImage(RectangularAdaptDensity): - - def __init__( - self, - shape: Tuple[int, int] = (3, 3), - weight_power: float = 1.0, - weight_floor: float = 0.0, - ): - """ - A uniform mesh of rectangular pixels, which without interpolation are paired with a 2D grid of (y,x) - coordinates. - - For a full description of how a mesh is paired with another grid, - see the :meth:`Pixelization API documentation `. - - The rectangular grid is uniform, has dimensions (total_y_pixels, total_x_pixels) and has indexing beginning - in the top-left corner and going rightwards and downwards. - - A ``Pixelization`` using a ``RectangularAdaptDensity`` mesh has three grids associated with it: - - - ``image_plane_data_grid``: The observed data grid in the image-plane (which is paired with the mesh in - the source-plane). - - ``source_plane_data_grid``: The observed data grid mapped to the source-plane after gravitational lensing. - - ``source_plane_mesh_grid``: The centres of each rectangular pixel. - - It does not have a ``image_plane_mesh_grid`` because a rectangular pixelization is constructed by overlaying - a grid of rectangular over the `source_plane_data_grid`. - - Each (y,x) coordinate in the `source_plane_data_grid` is associated with the rectangular pixelization pixel - it falls within. No interpolation is performed when making these associations. - Parameters - ---------- - shape - The 2D dimensions of the rectangular grid of pixels (total_y_pixels, total_x_pixel). - """ - - super().__init__(shape=shape) - - self.weight_power = weight_power - self.weight_floor = weight_floor - - def mesh_weight_map_from(self, adapt_data, xp=np) -> np.ndarray: - """ - The weight map of a rectangular pixelization is None, because magnificaiton adaption uses - the distribution and density of traced (y,x) coordinates in the source plane and - not weights or the adapt data. - - Parameters - ---------- - xp - The array library to use. - """ - mesh_weight_map = adapt_data.array - mesh_weight_map = xp.clip(mesh_weight_map, 1e-12, None) - mesh_weight_map = mesh_weight_map**self.weight_power - - # Apply floor using xp.where (safe for NumPy and JAX) - mesh_weight_map = xp.where( - mesh_weight_map < self.weight_floor, - self.weight_floor, - mesh_weight_map, + return self.interpolator_cls( + mesh=self, + data_grid=relocated_grid, + mesh_grid=Grid2DIrregular(mesh_grid), + mesh_weight_map=mesh_weight_map, + adapt_data=adapt_data, + preloads=preloads, + xp=xp, ) - - # Normalize - mesh_weight_map = mesh_weight_map / xp.sum(mesh_weight_map) - - return mesh_weight_map diff --git a/autoarray/inversion/mesh/mesh/rectangular_adapt_image.py b/autoarray/inversion/mesh/mesh/rectangular_adapt_image.py new file mode 100644 index 000000000..41a1badf1 --- /dev/null +++ b/autoarray/inversion/mesh/mesh/rectangular_adapt_image.py @@ -0,0 +1,75 @@ +import numpy as np +from typing import Tuple + +from autoarray.inversion.mesh.mesh.rectangular_adapt_density import ( + RectangularAdaptDensity, +) + + +class RectangularAdaptImage(RectangularAdaptDensity): + + def __init__( + self, + shape: Tuple[int, int] = (3, 3), + weight_power: float = 1.0, + weight_floor: float = 0.0, + ): + """ + A uniform mesh of rectangular pixels, which without interpolation are paired with a 2D grid of (y,x) + coordinates. + + For a full description of how a mesh is paired with another grid, + see the :meth:`Pixelization API documentation `. + + The rectangular grid is uniform, has dimensions (total_y_pixels, total_x_pixels) and has indexing beginning + in the top-left corner and going rightwards and downwards. + + A ``Pixelization`` using a ``RectangularAdaptDensity`` mesh has three grids associated with it: + + - ``image_plane_data_grid``: The observed data grid in the image-plane (which is paired with the mesh in + the source-plane). + - ``source_plane_data_grid``: The observed data grid mapped to the source-plane after gravitational lensing. + - ``source_plane_mesh_grid``: The centres of each rectangular pixel. + + It does not have a ``image_plane_mesh_grid`` because a rectangular pixelization is constructed by overlaying + a grid of rectangular over the `source_plane_data_grid`. + + Each (y,x) coordinate in the `source_plane_data_grid` is associated with the rectangular pixelization pixel + it falls within. No interpolation is performed when making these associations. + Parameters + ---------- + shape + The 2D dimensions of the rectangular grid of pixels (total_y_pixels, total_x_pixel). + """ + + super().__init__(shape=shape) + + self.weight_power = weight_power + self.weight_floor = weight_floor + + def mesh_weight_map_from(self, adapt_data, xp=np) -> np.ndarray: + """ + The weight map of a rectangular pixelization is None, because magnificaiton adaption uses + the distribution and density of traced (y,x) coordinates in the source plane and + not weights or the adapt data. + + Parameters + ---------- + xp + The array library to use. + """ + mesh_weight_map = adapt_data.array + mesh_weight_map = xp.clip(mesh_weight_map, 1e-12, None) + mesh_weight_map = mesh_weight_map**self.weight_power + + # Apply floor using xp.where (safe for NumPy and JAX) + mesh_weight_map = xp.where( + mesh_weight_map < self.weight_floor, + self.weight_floor, + mesh_weight_map, + ) + + # Normalize + mesh_weight_map = mesh_weight_map / xp.sum(mesh_weight_map) + + return mesh_weight_map diff --git a/autoarray/inversion/mesh/mesh/rectangular_uniform.py b/autoarray/inversion/mesh/mesh/rectangular_uniform.py new file mode 100644 index 000000000..6bf388987 --- /dev/null +++ b/autoarray/inversion/mesh/mesh/rectangular_uniform.py @@ -0,0 +1,14 @@ +from autoarray.inversion.mesh.mesh.rectangular_adapt_density import ( + RectangularAdaptDensity, +) + + +class RectangularUniform(RectangularAdaptDensity): + + @property + def interpolator_cls(self): + from autoarray.inversion.mesh.interpolator.rectangular_uniform import ( + InterpolatorRectangularUniform, + ) + + return InterpolatorRectangularUniform diff --git a/test_autoarray/structures/mesh/__init__.py b/autoarray/inversion/mesh/mesh_geometry/__init__.py similarity index 100% rename from test_autoarray/structures/mesh/__init__.py rename to autoarray/inversion/mesh/mesh_geometry/__init__.py diff --git a/autoarray/inversion/mesh/mesh_geometry/abstract.py b/autoarray/inversion/mesh/mesh_geometry/abstract.py new file mode 100644 index 000000000..a3a1f669c --- /dev/null +++ b/autoarray/inversion/mesh/mesh_geometry/abstract.py @@ -0,0 +1,12 @@ +import numpy as np + + +class AbstractMeshGeometry: + + def __init__(self, mesh, mesh_grid, data_grid, mesh_weight_map=None, xp=np): + + self.mesh = mesh + self.mesh_grid = mesh_grid + self.data_grid = data_grid + self.mesh_weight_map = mesh_weight_map + self._xp = xp diff --git a/autoarray/inversion/mesh/mesh_geometry/delaunay.py b/autoarray/inversion/mesh/mesh_geometry/delaunay.py new file mode 100644 index 000000000..f52c25394 --- /dev/null +++ b/autoarray/inversion/mesh/mesh_geometry/delaunay.py @@ -0,0 +1,207 @@ +import numpy as np +import scipy.spatial +from typing import Tuple + +from autoconf import cached_property + +from autoarray.geometry.geometry_2d_irregular import Geometry2DIrregular +from autoarray.inversion.linear_obj.neighbors import Neighbors +from autoarray.inversion.mesh.mesh_geometry.abstract import AbstractMeshGeometry + +from autoarray import exc + + +def voronoi_areas_numpy(points, qhull_options="Qbb Qc Qx Qm Q12 Pp"): + """ + Compute Voronoi cell areas with a fully optimized pure-NumPy pipeline. + Exact match to the per-cell SciPy Voronoi loop but much faster. + """ + import scipy.spatial + + vor = scipy.spatial.Voronoi(points, qhull_options=qhull_options) + + vertices = vor.vertices + point_region = vor.point_region + regions = vor.regions + N = len(point_region) + + # ------------------------------------------------------------ + # 1) Collect all region lists in one go (list comprehension is fast) + # ------------------------------------------------------------ + region_lists = [regions[r] for r in point_region] + + # Precompute which regions are unbounded (vectorized test) + unbounded = np.array([(-1 in r) for r in region_lists], dtype=bool) + + # Filter only bounded region vertex indices + clean_regions = [ + np.asarray([v for v in r if v != -1], dtype=int) for r in region_lists + ] + + # Compute lengths once + lengths = np.array([len(r) for r in clean_regions], dtype=int) + max_len = lengths.max() + + # ------------------------------------------------------------ + # 2) Build padded idx + mask in a vectorized-like way + # + # Instead of doing Python work inside the loop, we pre-pack + # the flattened data and then reshape. + # ------------------------------------------------------------ + idx = np.full((N, max_len), -1, dtype=int) + mask = np.zeros((N, max_len), dtype=bool) + + # Single loop remaining: extremely cheap + for i, (r, L) in enumerate(zip(clean_regions, lengths)): + if L: + idx[i, :L] = r + mask[i, :L] = True + + # ------------------------------------------------------------ + # 3) Gather polygon vertices (vectorized) + # ------------------------------------------------------------ + safe_idx = idx.clip(min=0) + verts = vertices[safe_idx] # (N, max_len, 2) + + # Extract x, y with masked invalid entries zeroed + x = np.where(mask, verts[..., 1], 0.0) + y = np.where(mask, verts[..., 0], 0.0) + + # ------------------------------------------------------------ + # 4) Vectorized "previous index" per polygon + # ------------------------------------------------------------ + safe_lengths = np.where(lengths == 0, 1, lengths) + j = np.arange(max_len) + prev = (j[None, :] - 1) % safe_lengths[:, None] + + # Efficient take-along-axis + x_prev = np.take_along_axis(x, prev, axis=1) + y_prev = np.take_along_axis(y, prev, axis=1) + + # ------------------------------------------------------------ + # 5) Shoelace vectorized + # ------------------------------------------------------------ + cross = x * y_prev - y * x_prev + areas = 0.5 * np.abs(cross.sum(axis=1)) + + # ------------------------------------------------------------ + # 6) Mark unbounded regions + # ------------------------------------------------------------ + areas[unbounded] = -1.0 + + return areas + + +class MeshGeometryDelaunay(AbstractMeshGeometry): + + @cached_property + def mesh_grid_xy(self): + """ + The default convention in `scipy.spatial` is to represent 2D coordinates as (x,y) pairs, whereas PyAutoArray + represents 2D coordinates as (y,x) pairs. + + Therefore, this property simply converts the (y,x) grid of irregular coordinates into an (x,y) grid. + """ + return self._xp.stack( + [self.mesh_grid.array[:, 0], self.mesh_grid.array[:, 1]] + ).T + + @property + def origin(self) -> Tuple[float, float]: + """ + The (y,x) origin of the Voronoi grid, which is fixed to (0.0, 0.0) for simplicity. + """ + return 0.0, 0.0 + + @property + def geometry(self): + shape_native_scaled = ( + np.amax(self[:, 0]).astype("float") - np.amin(self[:, 0]).astype("float"), + np.amax(self[:, 1]).astype("float") - np.amin(self[:, 1]).astype("float"), + ) + + scaled_maxima = ( + np.amax(self[:, 0]).astype("float"), + np.amax(self[:, 1]).astype("float"), + ) + + scaled_minima = ( + np.amin(self[:, 0]).astype("float"), + np.amin(self[:, 1]).astype("float"), + ) + + return Geometry2DIrregular( + shape_native_scaled=shape_native_scaled, + scaled_maxima=scaled_maxima, + scaled_minima=scaled_minima, + ) + + @cached_property + def neighbors(self) -> Neighbors: + """ + Returns a ndarray describing the neighbors of every pixel in a Delaunay triangulation, where a neighbor is + defined as two Delaunay triangles which are directly connected to one another in the triangulation. + + see `Neighbors` for a complete description of the neighboring scheme. + + The neighbors of a Voronoi mesh are computed using the `ridge_points` attribute of the scipy `Voronoi` + object, as described in the method `voronoi_neighbors_from`. + """ + + delaunay = scipy.spatial.Delaunay(self.mesh_grid_xy) + + indptr, indices = delaunay.vertex_neighbor_vertices + + sizes = indptr[1:] - indptr[:-1] + + neighbors = -1 * np.ones( + shape=(self.mesh_grid_xy.shape[0], int(np.max(sizes))), dtype="int" + ) + + for k in range(self.mesh_grid_xy.shape[0]): + neighbors[k][0 : sizes[k]] = indices[indptr[k] : indptr[k + 1]] + + return Neighbors(arr=neighbors.astype("int"), sizes=sizes.astype("int")) + + @cached_property + def voronoi(self) -> "scipy.spatial.Voronoi": + """ + Returns a `scipy.spatial.Voronoi` object from the 2D (y,x) grid of irregular coordinates, which correspond to + the centre of every Voronoi pixel. + + This object contains numerous attributes describing a Voronoi mesh. PyAutoArray uses + the `vertex_neighbor_vertices` attribute to determine the neighbors of every Delaunay triangle. + + There are numerous exceptions that `scipy.spatial.Delaunay` may raise when the input grid of coordinates used + to compute the Delaunay triangulation are ill posed. These exceptions are caught and combined into a single + `MeshException`, which helps exception handling in the `inversion` package. + """ + import scipy.spatial + from scipy.spatial import QhullError + + try: + return scipy.spatial.Voronoi( + self.mesh_grid_xy, + qhull_options="Qbb Qc Qx Qm", + ) + except (ValueError, OverflowError, QhullError) as e: + raise exc.MeshException() from e + + @property + def voronoi_areas(self): + return voronoi_areas_numpy(points=self.mesh_grid_xy) + + @property + def areas_for_magnification(self) -> np.ndarray: + """ + Returns the area of every Voronoi pixel in the Voronoi mesh. + + Pixels at boundaries can sometimes have large unrealistic areas, which can impact the magnification + calculation. This method therefore sets their areas to zero so they do not impact the magnification + calculation. + """ + areas = self.voronoi_areas + + areas[areas == -1] = 0.0 + + return areas diff --git a/autoarray/inversion/pixelization/mesh/mesh_util.py b/autoarray/inversion/mesh/mesh_geometry/rectangular.py similarity index 72% rename from autoarray/inversion/pixelization/mesh/mesh_util.py rename to autoarray/inversion/mesh/mesh_geometry/rectangular.py index 8ee5ba685..75475b40c 100644 --- a/autoarray/inversion/pixelization/mesh/mesh_util.py +++ b/autoarray/inversion/mesh/mesh_geometry/rectangular.py @@ -1,7 +1,10 @@ import numpy as np - from typing import List, Tuple +from autoarray.geometry.geometry_2d import Geometry2D +from autoarray.inversion.linear_obj.neighbors import Neighbors +from autoarray.inversion.mesh.mesh_geometry.abstract import AbstractMeshGeometry + def rectangular_neighbors_from( shape_native: Tuple[int, int], @@ -386,3 +389,147 @@ def rectangular_edge_pixel_list_from( # Sort and return return np.sort(edge_pixel_indices).tolist() + + +def adaptive_rectangular_areas_from( + source_grid_shape, data_grid, mesh_weight_map=None, xp=np +): + + from autoarray.inversion.mesh.interpolator.rectangular import ( + create_transforms, + ) + + edges_y = xp.linspace(1, 0, source_grid_shape[0] + 1) + edges_x = xp.linspace(0, 1, source_grid_shape[1] + 1) + + mu = data_grid.mean(axis=0) + scale = data_grid.std(axis=0).min() + source_grid_scaled = (data_grid - mu) / scale + + transform, inv_transform = create_transforms( + source_grid_scaled, mesh_weight_map=mesh_weight_map, xp=xp + ) + + def inv_full(U): + return inv_transform(U) * scale + mu + + pixel_edges = inv_full(xp.stack([edges_y, edges_x]).T) + pixel_lengths = xp.diff(pixel_edges, axis=0).squeeze() # shape (N_source, 2) + + dy = pixel_lengths[:, 0] + dx = pixel_lengths[:, 1] + + return xp.abs(xp.outer(dy, dx).flatten()) + + +class MeshGeometryRectangular(AbstractMeshGeometry): + + @property + def shape(self): + """ + The 2D dimensions of the rectangular pixelization with shape (y_pixels, x_pixel). + """ + return self.mesh.shape + + @property + def shape_native(self): + """ + The 2D dimensions of the rectangular pixelization with shape (y_pixels, x_pixel). + """ + return self.shape + + @property + def geometry(self): + + xmin = np.min(self.mesh_grid[:, 1]) + xmax = np.max(self.mesh_grid[:, 1]) + ymin = np.min(self.mesh_grid[:, 0]) + ymax = np.max(self.mesh_grid[:, 0]) + + pixel_scales = (ymax - ymin) / (self.shape[0] - 1), (xmax - xmin) / ( + self.shape[1] - 1 + ) + + origin = ((ymax + ymin) / 2.0, (xmax + xmin) / 2.0) + + return Geometry2D( + shape_native=self.shape_native, + pixel_scales=pixel_scales, + origin=origin, + ) + + @property + def pixel_scales(self) -> Tuple[float, float]: + return self.geometry.pixel_scales + + @property + def origin(self) -> Tuple[float, float]: + return self.geometry.origin + + @property + def areas_transformed(self): + """ + A class packing the ndarrays describing the neighbors of every pixel in the rectangular pixelization (see + `Neighbors` for a complete description of the neighboring scheme). + + The neighbors of a rectangular pixelization are computed by exploiting the uniform and symmetric nature of the + rectangular grid, as described in the method `rectangular_neighbors_from`. + """ + return adaptive_rectangular_areas_from( + source_grid_shape=self.shape_native, + data_grid=self.data_grid.over_sampled, + mesh_weight_map=self.mesh_weight_map, + xp=self._xp, + ) + + @property + def areas_for_magnification(self): + """ + The area of every pixel in the rectangular pixelization. + + Returns + ------- + ndarray + The area of every pixel in the rectangular pixelization. + """ + return self.areas_transformed + + @property + def edges_transformed(self): + """ + A class packing the ndarrays describing the neighbors of every pixel in the rectangular pixelization (see + `Neighbors` for a complete description of the neighboring scheme). + + The neighbors of a rectangular pixelization are computed by exploiting the uniform and symmetric nature of the + rectangular grid, as described in the method `rectangular_neighbors_from`. + """ + + from autoarray.inversion.mesh.interpolator.rectangular import ( + adaptive_rectangular_transformed_grid_from, + ) + + # edges defined in 0 -> 1 space, there is one more edge than pixel centers on each side + edges_y = self._xp.linspace(1, 0, self.shape_native[0] + 1) + edges_x = self._xp.linspace(0, 1, self.shape_native[1] + 1) + + edges_reshaped = self._xp.stack([edges_y, edges_x]).T + + return adaptive_rectangular_transformed_grid_from( + data_grid=self.data_grid.over_sampled, + grid=edges_reshaped, + mesh_weight_map=self.mesh_weight_map, + xp=self._xp, + ) + + @property + def neighbors(self) -> Neighbors: + """ + A class packing the ndarrays describing the neighbors of every pixel in the rectangular pixelization (see + `Neighbors` for a complete description of the neighboring scheme). + + The neighbors of a rectangular pixelization are computed by exploiting the uniform and symmetric nature of the + rectangular grid, as described in the method `rectangular_neighbors_from`. + """ + neighbors, sizes = rectangular_neighbors_from(shape_native=self.shape) + + return Neighbors(arr=neighbors.astype("int"), sizes=sizes.astype("int")) diff --git a/autoarray/inversion/mock/mock_image_mesh.py b/autoarray/inversion/mock/mock_image_mesh.py index bc87c72df..9608c9af4 100644 --- a/autoarray/inversion/mock/mock_image_mesh.py +++ b/autoarray/inversion/mock/mock_image_mesh.py @@ -4,7 +4,7 @@ from autoarray.mask.mask_2d import Mask2D from autoarray.structures.grids.irregular_2d import Grid2DIrregular -from autoarray.inversion.pixelization.image_mesh.abstract import AbstractImageMesh +from autoarray.inversion.mesh.image_mesh.abstract import AbstractImageMesh class MockImageMesh(AbstractImageMesh): diff --git a/autoarray/inversion/mock/mock_interpolator.py b/autoarray/inversion/mock/mock_interpolator.py new file mode 100644 index 000000000..03cb70bc1 --- /dev/null +++ b/autoarray/inversion/mock/mock_interpolator.py @@ -0,0 +1,28 @@ +from autoarray.inversion.mesh.interpolator.abstract import AbstractInterpolator + + +class MockInterpolator(AbstractInterpolator): + + def __init__(self, mappings=None, sizes=None, weights=None): + + self._mappings = mappings + self._sizes = sizes + self._weights = weights + + @property + def mappings(self): + if self._mappings is not None: + return self._mappings + return super().mappings + + @property + def sizes(self): + if self._sizes is not None: + return self._sizes + return super().sizes + + @property + def weights(self): + if self._weights is not None: + return self._weights + return super().weights diff --git a/autoarray/inversion/mock/mock_inversion.py b/autoarray/inversion/mock/mock_inversion.py index a73b3df1a..0477c7d0f 100644 --- a/autoarray/inversion/mock/mock_inversion.py +++ b/autoarray/inversion/mock/mock_inversion.py @@ -3,7 +3,7 @@ from autoarray.inversion.inversion.dataset_interface import DatasetInterface from autoarray.inversion.inversion.abstract import AbstractInversion -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings class MockInversion(AbstractInversion): @@ -30,14 +30,14 @@ def __init__( log_det_curvature_reg_matrix_term=None, log_det_regularization_matrix_term=None, fast_chi_squared: float = None, - settings: SettingsInversion = None, + settings: Settings = None, ): dataset = DatasetInterface( data=data, noise_map=noise_map, ) - settings = settings or SettingsInversion() + settings = settings or Settings() super().__init__( dataset=dataset, diff --git a/autoarray/inversion/mock/mock_inversion_imaging.py b/autoarray/inversion/mock/mock_inversion_imaging.py index 283d3152f..f6b59cc61 100644 --- a/autoarray/inversion/mock/mock_inversion_imaging.py +++ b/autoarray/inversion/mock/mock_inversion_imaging.py @@ -3,7 +3,7 @@ from autoarray.inversion.inversion.dataset_interface import DatasetInterface from autoarray.inversion.inversion.imaging.mapping import InversionImagingMapping -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings class MockInversionImaging(InversionImagingMapping): @@ -17,10 +17,10 @@ def __init__( operated_mapping_matrix=None, linear_func_operated_mapping_matrix_dict=None, data_linear_func_matrix_dict=None, - settings: SettingsInversion = None, + settings: Settings = None, ): - settings = settings or SettingsInversion() + settings = settings or Settings() dataset = DatasetInterface( data=data, diff --git a/autoarray/inversion/mock/mock_inversion_interferometer.py b/autoarray/inversion/mock/mock_inversion_interferometer.py index a25ec03f9..35b07f693 100644 --- a/autoarray/inversion/mock/mock_inversion_interferometer.py +++ b/autoarray/inversion/mock/mock_inversion_interferometer.py @@ -4,7 +4,7 @@ from autoarray.inversion.inversion.interferometer.mapping import ( InversionInterferometerMapping, ) -from autoarray.inversion.inversion.settings import SettingsInversion +from autoarray.settings import Settings class MockInversionInterferometer(InversionInterferometerMapping): @@ -15,7 +15,7 @@ def __init__( transformer=None, linear_obj_list=None, operated_mapping_matrix=None, - settings: SettingsInversion = None, + settings: Settings = None, ): dataset = DatasetInterface( data=data, @@ -23,7 +23,7 @@ def __init__( transformer=transformer, ) - settings = settings or SettingsInversion() + settings = settings or Settings() super().__init__( dataset=dataset, diff --git a/autoarray/inversion/mock/mock_mapper.py b/autoarray/inversion/mock/mock_mapper.py index 529155db8..b1f8414d4 100644 --- a/autoarray/inversion/mock/mock_mapper.py +++ b/autoarray/inversion/mock/mock_mapper.py @@ -1,42 +1,35 @@ import numpy as np -from typing import Optional, Tuple -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper -from autoarray.inversion.pixelization.mappers.mapper_grids import MapperGrids +from autoarray.inversion.mappers.abstract import Mapper -class MockMapper(AbstractMapper): +class MockMapper(Mapper): def __init__( self, - mask=None, + mesh=None, + mesh_geometry=None, + interpolator=None, source_plane_data_grid=None, source_plane_mesh_grid=None, - over_sampler=None, - border_relocator=None, adapt_data=None, + over_sampler=None, regularization=None, - pix_sub_weights=None, - pix_sub_weights_split_points=None, mapping_matrix=None, pixel_signals=None, parameters=None, ): - mapper_grids = MapperGrids( - mask=mask, - source_plane_data_grid=source_plane_data_grid, - source_plane_mesh_grid=source_plane_mesh_grid, - adapt_data=adapt_data, - ) super().__init__( - mapper_grids=mapper_grids, - border_relocator=border_relocator, + interpolator=interpolator, regularization=regularization, ) + self._mesh = mesh + self._source_plane_data_grid = source_plane_data_grid + self._source_plane_mesh_grid = source_plane_mesh_grid + self._adapt_data = adapt_data + self._mesh_geometry = mesh_geometry self._over_sampler = over_sampler - self._pix_sub_weights = pix_sub_weights - self._pix_sub_weights_split_points = pix_sub_weights_split_points self._mapping_matrix = mapping_matrix self._parameters = parameters self._pixel_signals = pixel_signals @@ -46,6 +39,30 @@ def pixel_signals_from(self, signal_scale, xp=np): return super().pixel_signals_from(signal_scale=signal_scale) return self._pixel_signals + @property + def source_plane_data_grid(self): + if self._source_plane_data_grid is None: + return super().source_plane_data_grid + return self._source_plane_data_grid + + @property + def source_plane_mesh_grid(self): + if self._source_plane_mesh_grid is None: + return super().source_plane_mesh_grid + return self._source_plane_mesh_grid + + @property + def adapt_data(self): + if self._adapt_data is None: + return super().adapt_data + return self._adapt_data + + @property + def mesh_geometry(self): + if self._mesh_geometry is None: + return super().mesh_geometry + return self._mesh_geometry + @property def params(self): if self._parameters is None: @@ -58,14 +75,6 @@ def over_sampler(self): return super().over_sampler return self._over_sampler - @property - def pix_sub_weights(self): - return self._pix_sub_weights - - @property - def pix_sub_weights_split_points(self): - return self._pix_sub_weights_split_points - @property def mapping_matrix(self): return self._mapping_matrix diff --git a/autoarray/inversion/mock/mock_mesh.py b/autoarray/inversion/mock/mock_mesh.py index 5ee3cea88..3214f5285 100644 --- a/autoarray/inversion/mock/mock_mesh.py +++ b/autoarray/inversion/mock/mock_mesh.py @@ -1,10 +1,10 @@ import numpy as np from typing import Optional +from autoarray.inversion.mock.mock_mapper import MockMapper from autoarray.mask.mask_2d import Mask2D -from autoarray.inversion.pixelization.mesh.abstract import AbstractMesh -from autoarray.structures.mesh.abstract_2d import Abstract2DMesh -from autoarray.inversion.pixelization.mappers.mapper_grids import MapperGrids +from autoarray.inversion.mesh.mesh.abstract import AbstractMesh +from autoarray.inversion.mesh.interpolator.abstract import AbstractInterpolator from autoarray.structures.grids.uniform_2d import Grid2D from autoarray.structures.grids.irregular_2d import Grid2DIrregular @@ -15,16 +15,16 @@ def __init__(self, image_plane_mesh_grid=None): self.image_plane_mesh_grid = image_plane_mesh_grid - def mapper_grids_from( + def interpolator_from( self, mask=None, source_plane_data_grid: Grid2D = None, border_relocator=None, - source_plane_mesh_grid: Optional[Abstract2DMesh] = None, + source_plane_mesh_grid: Optional[AbstractInterpolator] = None, image_plane_mesh_grid: Optional[Grid2DIrregular] = None, adapt_data: Optional[np.ndarray] = None, - ) -> MapperGrids: - return MapperGrids( + ): + return MockMapper( mask=mask, source_plane_data_grid=source_plane_data_grid, border_relocator=border_relocator, diff --git a/autoarray/inversion/mock/mock_pixelization.py b/autoarray/inversion/mock/mock_pixelization.py index c30bb3867..de8bca9e9 100644 --- a/autoarray/inversion/mock/mock_pixelization.py +++ b/autoarray/inversion/mock/mock_pixelization.py @@ -1,5 +1,5 @@ from autoarray.mask.mask_2d import Mask2D -from autoarray.inversion.pixelization.pixelization import Pixelization +from autoarray.inversion.pixelization import Pixelization class MockPixelization(Pixelization): @@ -16,7 +16,7 @@ def __init__( self.image_plane_mesh_grid = image_plane_mesh_grid # noinspection PyUnusedLocal,PyShadowingNames - def mapper_grids_from( + def interpolator_from( self, mask, source_plane_data_grid, diff --git a/autoarray/inversion/pixelization/pixelization.py b/autoarray/inversion/pixelization.py similarity index 91% rename from autoarray/inversion/pixelization/pixelization.py rename to autoarray/inversion/pixelization.py index 26bf776b6..73f17f90f 100644 --- a/autoarray/inversion/pixelization/pixelization.py +++ b/autoarray/inversion/pixelization.py @@ -1,7 +1,7 @@ from typing import Callable, Optional -from autoarray.inversion.pixelization.image_mesh.abstract import AbstractImageMesh -from autoarray.inversion.pixelization.mesh.abstract import AbstractMesh +from autoarray.inversion.mesh.image_mesh.abstract import AbstractImageMesh +from autoarray.inversion.mesh.mesh.abstract import AbstractMesh from autoarray.inversion.regularization.abstract import AbstractRegularization from autoarray import exc @@ -155,15 +155,6 @@ def __init__( self.mesh = mesh self.regularization = regularization - @property - def mapper_grids_from(self) -> Callable: - """ - Returns a ``MapperGrids`` object, which contains all of the different grids used by a - pixelization (``image_plane_data_grid``, ``image_plane_mesh_grid``, ``source_plane_data_grid``, - ``source_plane_mesh_grid``). - """ - return self.mesh.mapper_grids_from - def __repr__(self): string = "{}\n{}".format(self.__class__.__name__, str(self.mesh)) if self.regularization is not None: diff --git a/autoarray/inversion/pixelization/mappers/delaunay.py b/autoarray/inversion/pixelization/mappers/delaunay.py deleted file mode 100644 index 66cf9743b..000000000 --- a/autoarray/inversion/pixelization/mappers/delaunay.py +++ /dev/null @@ -1,231 +0,0 @@ -import numpy as np - -from autoconf import cached_property - -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper -from autoarray.inversion.pixelization.mappers.abstract import PixSubWeights - - -def triangle_area_xp(c0, c1, c2, xp): - """ - Twice triangle area using vector cross product magnitude. - Calling via xp ensures NumPy or JAX backend operation. - """ - v0 = c1 - c0 # (..., 2) - v1 = c2 - c0 - cross = v0[..., 0] * v1[..., 1] - v0[..., 1] * v1[..., 0] - return xp.abs(cross) - - -def pixel_weights_delaunay_from( - source_plane_data_grid, # (N_sub, 2) - source_plane_mesh_grid, # (N_pix, 2) - pix_indexes_for_sub_slim_index, # (N_sub, 3), padded with -1 - xp=np, # backend: np (default) or jnp -): - """ - XP-compatible (NumPy/JAX) version of pixel_weights_delaunay_from. - - Computes barycentric weights for Delaunay triangle interpolation. - """ - - N_sub = pix_indexes_for_sub_slim_index.shape[0] - - # ----------------------------- - # CASE MASKS - # ----------------------------- - # If pix_indexes_for_sub_slim_index[sub][1] == -1 → NOT in simplex - has_simplex = pix_indexes_for_sub_slim_index[:, 1] != -1 # (N_sub,) - - # ----------------------------- - # GATHER TRIANGLE VERTICES - # ----------------------------- - # Clip negatives (for padded entries) so that indexing doesn't crash - safe_indices = pix_indexes_for_sub_slim_index.clip(min=0) - - # (N_sub, 3, 2) - vertices = source_plane_mesh_grid[safe_indices] - - p0 = vertices[:, 0] # (N_sub, 2) - p1 = vertices[:, 1] - p2 = vertices[:, 2] - - # Query points - q = source_plane_data_grid # (N_sub, 2) - - # ----------------------------- - # TRIANGLE AREAS (barycentric numerators) - # ----------------------------- - a0 = triangle_area_xp(p1, p2, q, xp) - a1 = triangle_area_xp(p0, p2, q, xp) - a2 = triangle_area_xp(p0, p1, q, xp) - - area_sum = a0 + a1 + a2 - - # (N_sub, 3) - weights_bary = xp.stack([a0, a1, a2], axis=1) / area_sum[:, None] - - # ----------------------------- - # NEAREST-NEIGHBOUR CASE - # ----------------------------- - # For no-simplex: weight = [1,0,0] - weights_nn = xp.stack( - [ - xp.ones(N_sub), - xp.zeros(N_sub), - xp.zeros(N_sub), - ], - axis=1, - ) - - # ----------------------------- - # SELECT BETWEEN CASES - # ----------------------------- - pixel_weights = xp.where(has_simplex[:, None], weights_bary, weights_nn) - - return pixel_weights - - -class MapperDelaunay(AbstractMapper): - """ - To understand a `Mapper` one must be familiar `Mesh` objects and the `mesh` and `pixelization` packages, where - the four grids grouped in a `MapperGrids` object are explained (`image_plane_data_grid`, `source_plane_data_grid`, - `image_plane_mesh_grid`,`source_plane_mesh_grid`) - - If you are unfamliar withe above objects, read through the docstrings of the `pixelization`, `mesh` and - `mapper_grids` packages. - - A `Mapper` determines the mappings between the masked data grid's pixels (`image_plane_data_grid` and - `source_plane_data_grid`) and the pxelization's pixels (`image_plane_mesh_grid` and `source_plane_mesh_grid`). - - The 1D Indexing of each grid is identical in the `data` and `source` frames (e.g. the transformation does not - change the indexing, such that `source_plane_data_grid[0]` corresponds to the transformed value - of `image_plane_data_grid[0]` and so on). - - A mapper therefore only needs to determine the index mappings between the `grid_slim` and `mesh_grid`, - noting that associations are made by pairing `source_plane_mesh_grid` with `source_plane_data_grid`. - - Mappings are represented in the 2D ndarray `pix_indexes_for_sub_slim_index`, whereby the index of - a pixel on the `mesh_grid` maps to the index of a pixel on the `grid_slim` as follows: - - - pix_indexes_for_sub_slim_index[0, 0] = 0: the data's 1st sub-pixel maps to the pixelization's 1st pixel. - - pix_indexes_for_sub_slim_index[1, 0] = 3: the data's 2nd sub-pixel maps to the pixelization's 4th pixel. - - pix_indexes_for_sub_slim_index[2, 0] = 1: the data's 3rd sub-pixel maps to the pixelization's 2nd pixel. - - The second dimension of this array (where all three examples above are 0) is used for cases where a - single pixel on the `grid_slim` maps to multiple pixels on the `mesh_grid`. For example, using a - `Delaunay` pixelization, where every `grid_slim` pixel maps to three Delaunay pixels (the corners of the - triangles): - - - pix_indexes_for_sub_slim_index[0, 0] = 0: the data's 1st sub-pixel maps to the pixelization's 1st pixel. - - pix_indexes_for_sub_slim_index[0, 1] = 3: the data's 1st sub-pixel also maps to the pixelization's 4th pixel. - - pix_indexes_for_sub_slim_index[0, 2] = 5: the data's 1st sub-pixel also maps to the pixelization's 6th pixel. - - The mapper allows us to create a mapping matrix, which is a matrix representing the mapping between every - unmasked data pixel annd the pixels of a pixelization. This matrix is the basis of performing an `Inversion`, - which reconstructs the data using the `source_plane_mesh_grid`. - - Parameters - ---------- - mapper_grids - An object containing the data grid and mesh grid in both the data-frame and source-frame used by the - mapper to map data-points to linear object parameters. - regularization - The regularization scheme which may be applied to this linear object in order to smooth its solution, - which for a mapper smooths neighboring pixels on the mesh. - """ - - @property - def delaunay(self): - return self.source_plane_mesh_grid.delaunay - - @cached_property - def pix_sub_weights(self) -> PixSubWeights: - """ - Computes the following three quantities describing the mappings between of every sub-pixel in the masked data - and pixel in the `Delaunay` pixelization. - - - `pix_indexes_for_sub_slim_index`: the mapping of every data pixel (given its `sub_slim_index`) - to pixelization pixels (given their `pix_indexes`). - - - `pix_sizes_for_sub_slim_index`: the number of mappings of every data pixel to pixelization pixels. - - - `pix_weights_for_sub_slim_index`: the interpolation weights of every data pixel's pixelization - pixel mapping - - These are packaged into the class `PixSubWeights` with attributes `mappings`, `sizes` and `weights`. - - The `sub_slim_index` refers to the masked data sub-pixels and `pix_indexes` the pixelization pixel indexes, - for example: - - - `pix_indexes_for_sub_slim_index[0, 0] = 2`: The data's first (index 0) sub-pixel maps to the RectangularAdaptDensity - pixelization's third (index 2) pixel. - - - `pix_indexes_for_sub_slim_index[2, 0] = 4`: The data's third (index 2) sub-pixel maps to the RectangularAdaptDensity - pixelization's fifth (index 4) pixel. - - The second dimension of the array `pix_indexes_for_sub_slim_index`, which is 0 in both examples above, is used - for cases where a data pixel maps to more than one pixelization pixel. - - For a `Delaunay` pixelization each data pixel maps to 3 Delaunay triangles with interpolation, for example: - - - `pix_indexes_for_sub_slim_index[0, 0] = 2`: The data's first (index 0) sub-pixel maps to the Delaunay - pixelization's third (index 2) pixel. - - - `pix_indexes_for_sub_slim_index[0, 1] = 5`: The data's first (index 0) sub-pixel also maps to the Delaunay - pixelization's sixth (index 5) pixel. - - - `pix_indexes_for_sub_slim_index[0, 2] = 8`: The data's first (index 0) sub-pixel also maps to the Delaunay - pixelization's ninth (index 8) pixel. - - The interpolation weights of these multiple mappings are stored in the array `pix_weights_for_sub_slim_index`. - - For the Delaunay pixelization these mappings are calculated using the Scipy spatial library - (see `mapper_numba_util.pix_indexes_for_sub_slim_index_delaunay_from`). - """ - delaunay = self.delaunay - - mappings = delaunay.mappings.astype("int") - sizes = delaunay.sizes.astype("int") - - weights = pixel_weights_delaunay_from( - source_plane_data_grid=self.source_plane_data_grid.over_sampled, - source_plane_mesh_grid=self.source_plane_mesh_grid.array, - pix_indexes_for_sub_slim_index=mappings, - xp=self._xp, - ) - - return PixSubWeights(mappings=mappings, sizes=sizes, weights=weights) - - @property - def pix_sub_weights_split_points(self) -> PixSubWeights: - """ - The property `pix_sub_weights` property describes the calculation of the `PixSubWeights` object, which contains - numpy arrays describing how data-points and mapper pixels map to one another and the weights of these mappings. - - For certain regularization schemes (e.g. `ConstantSplit`, `AdaptiveBrightnessSplit`) regularization uses - mappings which are split in a cross configuration in order to factor in the derivative of the mapper - reconstruction. - - This property returns a unique set of `PixSubWeights` used for these regularization schemes which compute - mappings and weights at each point on the split cross. - """ - delaunay = self.delaunay - - splitted_weights = pixel_weights_delaunay_from( - source_plane_data_grid=delaunay.split_points, - source_plane_mesh_grid=self.source_plane_mesh_grid.array, - pix_indexes_for_sub_slim_index=delaunay.splitted_mappings.astype("int"), - xp=self._xp, - ) - - append_line_int = np.zeros((len(splitted_weights), 1), dtype="int") - 1 - append_line_float = np.zeros((len(splitted_weights), 1), dtype="float") - - return PixSubWeights( - mappings=self._xp.hstack( - (delaunay.splitted_mappings.astype(self._xp.int32), append_line_int) - ), - sizes=delaunay.splitted_sizes.astype(self._xp.int32), - weights=self._xp.hstack((splitted_weights, append_line_float)), - ) diff --git a/autoarray/inversion/pixelization/mappers/factory.py b/autoarray/inversion/pixelization/mappers/factory.py deleted file mode 100644 index a7718e150..000000000 --- a/autoarray/inversion/pixelization/mappers/factory.py +++ /dev/null @@ -1,79 +0,0 @@ -import numpy as np -from typing import Optional - -from autoarray.inversion.pixelization.mappers.mapper_grids import MapperGrids -from autoarray.inversion.pixelization.border_relocator import BorderRelocator -from autoarray.inversion.regularization.abstract import AbstractRegularization -from autoarray.inversion.inversion.settings import SettingsInversion -from autoarray.structures.mesh.rectangular_2d import Mesh2DRectangular -from autoarray.structures.mesh.rectangular_2d_uniform import Mesh2DRectangularUniform -from autoarray.structures.mesh.delaunay_2d import Mesh2DDelaunay - - -def mapper_from( - mapper_grids: MapperGrids, - regularization: Optional[AbstractRegularization], - border_relocator: Optional[BorderRelocator] = None, - settings=SettingsInversion(), - preloads=None, - xp=np, -): - """ - Factory which given input `MapperGrids` and `Regularization` objects creates a `Mapper`. - - A `Mapper` determines the mappings between a masked dataset's pixels and pixels of a linear object pixelization. - The mapper is used in order to fit a dataset via an inversion. Docstrings in the packages `linear_obj`, `mesh`, - `pixelization`, `mapper_grids` `mapper` and `inversion` provide more details. - - This factory inspects the type of mesh contained in the `MapperGrids` and uses this to determine the type of - `Mapper` it creates. For example, if a Delaunay mesh is used, a `MapperDelaunay` is created. - - Parameters - ---------- - mapper_grids - An object containing the data grid and mesh grid in both the data-frame and source-frame used by the - mapper to map data-points to linear object parameters. - regularization - The regularization scheme which may be applied to this linear object in order to smooth its solution, - which for a mapper smooths neighboring pixels on the mesh. - - Returns - ------- - A mapper whose type is determined by the input `mapper_grids` mesh type. - """ - - from autoarray.inversion.pixelization.mappers.rectangular import ( - MapperRectangular, - ) - from autoarray.inversion.pixelization.mappers.rectangular_uniform import ( - MapperRectangularUniform, - ) - from autoarray.inversion.pixelization.mappers.delaunay import MapperDelaunay - - if isinstance(mapper_grids.source_plane_mesh_grid, Mesh2DRectangularUniform): - return MapperRectangularUniform( - mapper_grids=mapper_grids, - border_relocator=border_relocator, - regularization=regularization, - settings=settings, - preloads=preloads, - xp=xp, - ) - elif isinstance(mapper_grids.source_plane_mesh_grid, Mesh2DRectangular): - return MapperRectangular( - mapper_grids=mapper_grids, - border_relocator=border_relocator, - regularization=regularization, - settings=settings, - preloads=preloads, - xp=xp, - ) - elif isinstance(mapper_grids.source_plane_mesh_grid, Mesh2DDelaunay): - return MapperDelaunay( - mapper_grids=mapper_grids, - border_relocator=border_relocator, - regularization=regularization, - settings=settings, - preloads=preloads, - xp=xp, - ) diff --git a/autoarray/inversion/pixelization/mappers/mapper_grids.py b/autoarray/inversion/pixelization/mappers/mapper_grids.py deleted file mode 100644 index 59903d03c..000000000 --- a/autoarray/inversion/pixelization/mappers/mapper_grids.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations -import numpy as np -from typing import TYPE_CHECKING, Dict, Optional - -from autoarray.mask.mask_2d import Mask2D -from autoarray.structures.arrays.uniform_2d import Array2D -from autoarray.structures.grids.uniform_2d import Grid2D -from autoarray.structures.grids.irregular_2d import Grid2DIrregular -from autoarray.structures.mesh.abstract_2d import Abstract2DMesh - -from autoarray.structures.grids import grid_2d_util - - -class MapperGrids: - def __init__( - self, - mask: Mask2D, - source_plane_data_grid: Grid2D, - source_plane_mesh_grid: Optional[Abstract2DMesh] = None, - image_plane_mesh_grid: Optional[Grid2DIrregular] = None, - adapt_data: Optional[np.ndarray] = None, - mesh_weight_map: Optional[Array2D] = None, - ): - """ - Groups the different grids used by `Mesh` objects, the `mesh` package and the `pixelization` package, which - create the following four grids: - - - `image_plane_data_grid`: the grid defining where data-points in frame of the data are. - - - `source_plane_data_grid`: the grid defining where the mapped coordinates of these data-points in the source-frame - of the linear object are. - - - `image_plane_mesh_grid`: the grid defining where the linear object parameters (e.g. what are used as pixels of - the mapper) are in the image-plane. - - - `source_plane_mesh_grid`: the grid defining where the mapped coordinates of the linear object parameters - are in the source frame. - - Read the docstrings of the `mesh` package for more information is this is unclear. - - This grouped set of grids are input into `Mapper` objects, in order to determine the mappings between the - masked data grid's data points (`image_plane_data_grid` and `source_plane_data_grid`) and the mesh's pixels - (`image_plane_mesh_grid` and `source_plane_mesh_grid`). - - Parameters - ---------- - source_plane_data_grid - A 2D grid of (y,x) coordinates associated with the unmasked 2D data after it has been transformed to the - `source` reference frame. - source_plane_mesh_grid - The 2D grid of (y,x) centres of every pixelization pixel in the `source` frame. - image_plane_mesh_grid - The sparse set of (y,x) coordinates computed from the unmasked data in the `data` frame. This has a - transformation applied to it to create the `source_plane_mesh_grid`. - adapt_data - An image which is used to determine the `image_plane_mesh_grid` and therefore adapt the distribution of - pixels of the Delaunay grid to the data it discretizes. - mesh_weight_map - The weight map used to weight the creation of the rectangular mesh grid, which is used for the - `RectangularBrightness` mesh which adapts the size of its pixels to where the source is reconstructed. - """ - - self.mask = mask - self.source_plane_data_grid = source_plane_data_grid - self.source_plane_mesh_grid = source_plane_mesh_grid - self.image_plane_mesh_grid = image_plane_mesh_grid - self.adapt_data = adapt_data - self.mesh_weight_map = mesh_weight_map - - @property - def image_plane_data_grid(self): - return self.mask.derive_grid.unmasked - - @property - def mesh_pixels_per_image_pixels(self): - - mesh_pixels_per_image_pixels = grid_2d_util.grid_pixels_in_mask_pixels_from( - grid=np.array(self.image_plane_mesh_grid), - shape_native=self.mask.shape_native, - pixel_scales=self.mask.pixel_scales, - origin=self.mask.origin, - ) - - return Array2D( - values=mesh_pixels_per_image_pixels, - mask=self.mask, - ) diff --git a/autoarray/inversion/pixelization/mappers/rectangular.py b/autoarray/inversion/pixelization/mappers/rectangular.py deleted file mode 100644 index cf9e5290a..000000000 --- a/autoarray/inversion/pixelization/mappers/rectangular.py +++ /dev/null @@ -1,164 +0,0 @@ -from typing import Tuple - -from autoconf import cached_property - -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper -from autoarray.inversion.pixelization.mappers.abstract import PixSubWeights - -from autoarray.inversion.pixelization.mappers import mapper_util - - -class MapperRectangular(AbstractMapper): - """ - To understand a `Mapper` one must be familiar `Mesh` objects and the `mesh` and `pixelization` packages, where - the four grids grouped in a `MapperGrids` object are explained (`image_plane_data_grid`, `source_plane_data_grid`, - `image_plane_mesh_grid`,`source_plane_mesh_grid`) - - If you are unfamliar withe above objects, read through the docstrings of the `pixelization`, `mesh` and - `mapper_grids` packages. - - A `Mapper` determines the mappings between the masked data grid's pixels (`image_plane_data_grid` and - `source_plane_data_grid`) and the mesh's pixels (`image_plane_mesh_grid` and `source_plane_mesh_grid`). - - The 1D Indexing of each grid is identical in the `data` and `source` frames (e.g. the transformation does not - change the indexing, such that `source_plane_data_grid[0]` corresponds to the transformed value - of `image_plane_data_grid[0]` and so on). - - A mapper therefore only needs to determine the index mappings between the `grid_slim` and `mesh_grid`, - noting that associations are made by pairing `source_plane_mesh_grid` with `source_plane_data_grid`. - - Mappings are represented in the 2D ndarray `pix_indexes_for_sub_slim_index`, whereby the index of - a pixel on the `mesh_grid` maps to the index of a pixel on the `grid_slim` as follows: - - - pix_indexes_for_sub_slim_index[0, 0] = 0: the data's 1st sub-pixel maps to the mesh's 1st pixel. - - pix_indexes_for_sub_slim_index[1, 0] = 3: the data's 2nd sub-pixel maps to the mesh's 4th pixel. - - pix_indexes_for_sub_slim_index[2, 0] = 1: the data's 3rd sub-pixel maps to the mesh's 2nd pixel. - - The second dimension of this array (where all three examples above are 0) is used for cases where a - single pixel on the `grid_slim` maps to multiple pixels on the `mesh_grid`. For example, a - `Delaunay` triangulation, where every `grid_slim` pixel maps to three Delaunay pixels (the corners of the - triangles) with varying interpolation weights . - - For a `RectangularAdaptDensity` mesh every pixel in the masked data maps to only one pixel, thus the second - dimension of `pix_indexes_for_sub_slim_index` is always of size 1. - - The mapper allows us to create a mapping matrix, which is a matrix representing the mapping between every - unmasked data pixel annd the pixels of a mesh. This matrix is the basis of performing an `Inversion`, - which reconstructs the data using the `source_plane_mesh_grid`. - - Parameters - ---------- - mapper_grids - An object containing the data grid and mesh grid in both the data-frame and source-frame used by the - mapper to map data-points to linear object parameters. - regularization - The regularization scheme which may be applied to this linear object in order to smooth its solution, - which for a mapper smooths neighboring pixels on the mesh. - """ - - @property - def shape_native(self) -> Tuple[int, ...]: - return self.source_plane_mesh_grid.shape_native - - @cached_property - def pix_sub_weights(self) -> PixSubWeights: - """ - Computes the following three quantities describing the mappings between of every sub-pixel in the masked data - and pixel in the `RectangularAdaptDensity` mesh. - - - `pix_indexes_for_sub_slim_index`: the mapping of every data pixel (given its `sub_slim_index`) - to mesh pixels (given their `pix_indexes`). - - - `pix_sizes_for_sub_slim_index`: the number of mappings of every data pixel to mesh pixels. - - - `pix_weights_for_sub_slim_index`: the interpolation weights of every data pixel's mesh - pixel mapping - - These are packaged into the class `PixSubWeights` with attributes `mappings`, `sizes` and `weights`. - - The `sub_slim_index` refers to the masked data sub-pixels and `pix_indexes` the mesh pixel indexes, - for example: - - - `pix_indexes_for_sub_slim_index[0, 0] = 2`: The data's first (index 0) sub-pixel maps to the RectangularAdaptDensity - mesh's third (index 2) pixel. - - - `pix_indexes_for_sub_slim_index[2, 0] = 4`: The data's third (index 2) sub-pixel maps to the RectangularAdaptDensity - mesh's fifth (index 4) pixel. - - The second dimension of the array `pix_indexes_for_sub_slim_index`, which is 0 in both examples above, is used - for cases where a data pixel maps to more than one mesh pixel (for example a `Delaunay` triangulation - where each data pixel maps to 3 Delaunay triangles with interpolation weights). The weights of multiple mappings - are stored in the array `pix_weights_for_sub_slim_index`. - - For a RectangularAdaptDensity pixelization each data sub-pixel maps to a single mesh pixel, thus the second - dimension of the array `pix_indexes_for_sub_slim_index` 1 and all entries in `pix_weights_for_sub_slim_index` - are equal to 1.0. - """ - mappings, weights = ( - mapper_util.adaptive_rectangular_mappings_weights_via_interpolation_from( - source_grid_size=self.shape_native[0], - source_plane_data_grid=self.source_plane_data_grid.array, - source_plane_data_grid_over_sampled=self._xp.array( - self.source_plane_data_grid.over_sampled - ), - mesh_weight_map=self.mapper_grids.mesh_weight_map, - xp=self._xp, - ) - ) - - return PixSubWeights( - mappings=mappings, - sizes=4 * self._xp.ones(len(mappings), dtype="int"), - weights=weights, - ) - - @property - def areas_transformed(self): - """ - A class packing the ndarrays describing the neighbors of every pixel in the rectangular pixelization (see - `Neighbors` for a complete description of the neighboring scheme). - - The neighbors of a rectangular pixelization are computed by exploiting the uniform and symmetric nature of the - rectangular grid, as described in the method `mesh_util.rectangular_neighbors_from`. - """ - return mapper_util.adaptive_rectangular_areas_from( - source_grid_shape=self.shape_native, - source_plane_data_grid=self.source_plane_data_grid.array, - mesh_weight_map=self.mapper_grids.mesh_weight_map, - xp=self._xp, - ) - - @property - def areas_for_magnification(self): - """ - The area of every pixel in the rectangular pixelization. - - Returns - ------- - ndarray - The area of every pixel in the rectangular pixelization. - """ - return self.areas_transformed - - @property - def edges_transformed(self): - """ - A class packing the ndarrays describing the neighbors of every pixel in the rectangular pixelization (see - `Neighbors` for a complete description of the neighboring scheme). - - The neighbors of a rectangular pixelization are computed by exploiting the uniform and symmetric nature of the - rectangular grid, as described in the method `mesh_util.rectangular_neighbors_from`. - """ - - # edges defined in 0 -> 1 space, there is one more edge than pixel centers on each side - edges_y = self._xp.linspace(1, 0, self.shape_native[0] + 1) - edges_x = self._xp.linspace(0, 1, self.shape_native[1] + 1) - - edges_reshaped = self._xp.stack([edges_y, edges_x]).T - - return mapper_util.adaptive_rectangular_transformed_grid_from( - source_plane_data_grid=self.source_plane_data_grid.array, - grid=edges_reshaped, - mesh_weight_map=self.mapper_grids.mesh_weight_map, - xp=self._xp, - ) diff --git a/autoarray/inversion/pixelization/mappers/rectangular_uniform.py b/autoarray/inversion/pixelization/mappers/rectangular_uniform.py deleted file mode 100644 index a82be5a7d..000000000 --- a/autoarray/inversion/pixelization/mappers/rectangular_uniform.py +++ /dev/null @@ -1,103 +0,0 @@ -from autoarray.inversion.pixelization.mappers.rectangular import MapperRectangular -from autoarray.inversion.pixelization.mappers.abstract import PixSubWeights - -from autoarray.inversion.pixelization.mappers import mapper_util - - -class MapperRectangularUniform(MapperRectangular): - """ - To understand a `Mapper` one must be familiar `Mesh` objects and the `mesh` and `pixelization` packages, where - the four grids grouped in a `MapperGrids` object are explained (`image_plane_data_grid`, `source_plane_data_grid`, - `image_plane_mesh_grid`,`source_plane_mesh_grid`) - - If you are unfamliar withe above objects, read through the docstrings of the `pixelization`, `mesh` and - `mapper_grids` packages. - - A `Mapper` determines the mappings between the masked data grid's pixels (`image_plane_data_grid` and - `source_plane_data_grid`) and the mesh's pixels (`image_plane_mesh_grid` and `source_plane_mesh_grid`). - - The 1D Indexing of each grid is identical in the `data` and `source` frames (e.g. the transformation does not - change the indexing, such that `source_plane_data_grid[0]` corresponds to the transformed value - of `image_plane_data_grid[0]` and so on). - - A mapper therefore only needs to determine the index mappings between the `grid_slim` and `mesh_grid`, - noting that associations are made by pairing `source_plane_mesh_grid` with `source_plane_data_grid`. - - Mappings are represented in the 2D ndarray `pix_indexes_for_sub_slim_index`, whereby the index of - a pixel on the `mesh_grid` maps to the index of a pixel on the `grid_slim` as follows: - - - pix_indexes_for_sub_slim_index[0, 0] = 0: the data's 1st sub-pixel maps to the mesh's 1st pixel. - - pix_indexes_for_sub_slim_index[1, 0] = 3: the data's 2nd sub-pixel maps to the mesh's 4th pixel. - - pix_indexes_for_sub_slim_index[2, 0] = 1: the data's 3rd sub-pixel maps to the mesh's 2nd pixel. - - The second dimension of this array (where all three examples above are 0) is used for cases where a - single pixel on the `grid_slim` maps to multiple pixels on the `mesh_grid`. For example, a - `Delaunay` triangulation, where every `grid_slim` pixel maps to three Delaunay pixels (the corners of the - triangles) with varying interpolation weights . - - For a `RectangularAdaptDensity` mesh every pixel in the masked data maps to only one pixel, thus the second - dimension of `pix_indexes_for_sub_slim_index` is always of size 1. - - The mapper allows us to create a mapping matrix, which is a matrix representing the mapping between every - unmasked data pixel annd the pixels of a mesh. This matrix is the basis of performing an `Inversion`, - which reconstructs the data using the `source_plane_mesh_grid`. - - Parameters - ---------- - mapper_grids - An object containing the data grid and mesh grid in both the data-frame and source-frame used by the - mapper to map data-points to linear object parameters. - regularization - The regularization scheme which may be applied to this linear object in order to smooth its solution, - which for a mapper smooths neighboring pixels on the mesh. - """ - - @property - def pix_sub_weights(self) -> PixSubWeights: - """ - Computes the following three quantities describing the mappings between of every sub-pixel in the masked data - and pixel in the `RectangularAdaptDensity` mesh. - - - `pix_indexes_for_sub_slim_index`: the mapping of every data pixel (given its `sub_slim_index`) - to mesh pixels (given their `pix_indexes`). - - - `pix_sizes_for_sub_slim_index`: the number of mappings of every data pixel to mesh pixels. - - - `pix_weights_for_sub_slim_index`: the interpolation weights of every data pixel's mesh - pixel mapping - - These are packaged into the class `PixSubWeights` with attributes `mappings`, `sizes` and `weights`. - - The `sub_slim_index` refers to the masked data sub-pixels and `pix_indexes` the mesh pixel indexes, - for example: - - - `pix_indexes_for_sub_slim_index[0, 0] = 2`: The data's first (index 0) sub-pixel maps to the RectangularAdaptDensity - mesh's third (index 2) pixel. - - - `pix_indexes_for_sub_slim_index[2, 0] = 4`: The data's third (index 2) sub-pixel maps to the RectangularAdaptDensity - mesh's fifth (index 4) pixel. - - The second dimension of the array `pix_indexes_for_sub_slim_index`, which is 0 in both examples above, is used - for cases where a data pixel maps to more than one mesh pixel (for example a `Delaunay` triangulation - where each data pixel maps to 3 Delaunay triangles with interpolation weights). The weights of multiple mappings - are stored in the array `pix_weights_for_sub_slim_index`. - - For a RectangularAdaptDensity pixelization each data sub-pixel maps to a single mesh pixel, thus the second - dimension of the array `pix_indexes_for_sub_slim_index` 1 and all entries in `pix_weights_for_sub_slim_index` - are equal to 1.0. - """ - - mappings, weights = ( - mapper_util.rectangular_mappings_weights_via_interpolation_from( - shape_native=self.shape_native, - source_plane_mesh_grid=self.source_plane_mesh_grid.array, - source_plane_data_grid=self.source_plane_data_grid.over_sampled, - xp=self._xp, - ) - ) - - return PixSubWeights( - mappings=mappings, - sizes=4 * self._xp.ones(len(mappings), dtype="int"), - weights=weights, - ) diff --git a/autoarray/inversion/pixelization/mesh/__init__.py b/autoarray/inversion/pixelization/mesh/__init__.py deleted file mode 100644 index 5c7a61ba9..000000000 --- a/autoarray/inversion/pixelization/mesh/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .abstract import AbstractMesh as Mesh -from .rectangular import RectangularAdaptDensity -from .rectangular import RectangularAdaptImage -from .rectangular_uniform import RectangularUniform -from .delaunay import Delaunay diff --git a/autoarray/inversion/pixelization/mesh/rectangular_uniform.py b/autoarray/inversion/pixelization/mesh/rectangular_uniform.py deleted file mode 100644 index 2c95f225b..000000000 --- a/autoarray/inversion/pixelization/mesh/rectangular_uniform.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy as np -from typing import Optional - -from autoarray.inversion.pixelization.mesh.rectangular import RectangularAdaptDensity - -from autoarray.structures.grids.irregular_2d import Grid2DIrregular -from autoarray.structures.grids.uniform_2d import Grid2D -from autoarray.structures.mesh.rectangular_2d_uniform import Mesh2DRectangularUniform - - -class RectangularUniform(RectangularAdaptDensity): - - def mesh_grid_from( - self, - source_plane_data_grid: Optional[Grid2D] = None, - source_plane_mesh_grid: Optional[Grid2D] = None, - xp=np, - ) -> Mesh2DRectangularUniform: - """ - Return the rectangular `source_plane_mesh_grid` as a `Mesh2DRectangular` object, which provides additional - functionality for perform operatons that exploit the geometry of a rectangular pixelization. - - Parameters - ---------- - source_plane_data_grid - The (y,x) grid of coordinates over which the rectangular pixelization is overlaid, where this grid may have - had exterior pixels relocated to its edge via the border. - source_plane_mesh_grid - Not used for a rectangular pixelization, because the pixelization grid in the `source` frame is computed - by overlaying the `source_plane_data_grid` with the rectangular pixelization. - """ - return Mesh2DRectangularUniform.overlay_grid( - shape_native=self.shape, - grid=Grid2DIrregular(source_plane_data_grid.over_sampled), - xp=xp, - ) diff --git a/autoarray/inversion/plot/inversion_plotters.py b/autoarray/inversion/plot/inversion_plotters.py index 0db63f7b2..8faf23768 100644 --- a/autoarray/inversion/plot/inversion_plotters.py +++ b/autoarray/inversion/plot/inversion_plotters.py @@ -2,7 +2,7 @@ from autoconf import conf -from autoarray.inversion.pixelization.mappers.abstract import AbstractMapper +from autoarray.inversion.mappers.abstract import Mapper from autoarray.plot.abstract_plotters import AbstractPlotter from autoarray.plot.visuals.two_d import Visuals2D from autoarray.plot.mat_plot.two_d import MatPlot2D @@ -61,7 +61,7 @@ def mapper_plotter_from(self, mapper_index: int) -> MapperPlotter: An object that plots mappers which is used for plotting attributes of the inversion. """ return MapperPlotter( - mapper=self.inversion.cls_list_from(cls=AbstractMapper)[mapper_index], + mapper=self.inversion.cls_list_from(cls=Mapper)[mapper_index], mat_plot_2d=self.mat_plot_2d, visuals_2d=self.visuals_2d, ) @@ -137,7 +137,7 @@ def figures_2d_of_pixelization( the brightest regions of the galaxies being plotted as opposed to the full extent of the grid. """ - if not self.inversion.has(cls=AbstractMapper): + if not self.inversion.has(cls=Mapper): return mapper_plotter = self.mapper_plotter_from(mapper_index=pixelization_index) @@ -267,7 +267,7 @@ def figures_2d_of_pixelization( if mesh_pixels_per_image_pixels: try: mesh_pixels_per_image_pixels = ( - mapper_plotter.mapper.mapper_grids.mesh_pixels_per_image_pixels + mapper_plotter.mapper.mesh_pixels_per_image_pixels ) self.mat_plot_2d.plot_array( @@ -332,11 +332,9 @@ def subplot_of_mapper( self.mat_plot_2d.use_log10 = False - mapper = self.inversion.cls_list_from(cls=AbstractMapper)[mapper_index] + mapper = self.inversion.cls_list_from(cls=Mapper)[mapper_index] - self.visuals_2d += Visuals2D( - mesh_grid=mapper.mapper_grids.image_plane_mesh_grid - ) + self.visuals_2d += Visuals2D(mesh_grid=mapper.image_plane_mesh_grid) self.set_title(label="Mesh Pixel Grid Overlaid") self.figures_2d_of_pixelization( @@ -411,7 +409,7 @@ def subplot_mappings( "total_mappings_pixels" ] - mapper = self.inversion.cls_list_from(cls=AbstractMapper)[pixelization_index] + mapper = self.inversion.cls_list_from(cls=Mapper)[pixelization_index] pix_indexes = self.inversion.max_pixel_list_from( total_pixels=total_pixels, diff --git a/autoarray/inversion/plot/mapper_plotters.py b/autoarray/inversion/plot/mapper_plotters.py index 83cfb79a3..b9a446792 100644 --- a/autoarray/inversion/plot/mapper_plotters.py +++ b/autoarray/inversion/plot/mapper_plotters.py @@ -1,14 +1,10 @@ import numpy as np -from typing import Union from autoarray.plot.abstract_plotters import AbstractPlotter from autoarray.plot.visuals.two_d import Visuals2D from autoarray.plot.mat_plot.two_d import MatPlot2D from autoarray.plot.auto_labels import AutoLabels from autoarray.structures.arrays.uniform_2d import Array2D -from autoarray.inversion.pixelization.mappers.rectangular import ( - MapperRectangular, -) import logging @@ -18,7 +14,7 @@ class MapperPlotter(AbstractPlotter): def __init__( self, - mapper: MapperRectangular, + mapper, mat_plot_2d: MatPlot2D = None, visuals_2d: Visuals2D = None, ): @@ -69,7 +65,7 @@ def figure_2d_image(self, image): self.mat_plot_2d.plot_array( array=image, visuals_2d=self.visuals_2d, - grid_indexes=self.mapper.mapper_grids.image_plane_data_grid.over_sampled, + grid_indexes=self.mapper.image_plane_data_grid.over_sampled, auto_labels=AutoLabels( title="Image (Image-Plane)", filename="mapper_image" ), diff --git a/autoarray/inversion/regularization/__init__.py b/autoarray/inversion/regularization/__init__.py index c5d696062..aebffb8b0 100644 --- a/autoarray/inversion/regularization/__init__.py +++ b/autoarray/inversion/regularization/__init__.py @@ -3,11 +3,11 @@ from .constant import Constant from .constant_zeroth import ConstantZeroth from .constant_split import ConstantSplit -from .adaptive_brightness import AdaptiveBrightness -from .adaptive_brightness_split import AdaptiveBrightnessSplit +from .adapt import Adapt +from .adapt_split import AdaptSplit from .brightness_zeroth import BrightnessZeroth -from .adaptive_brightness_split_zeroth import AdaptiveBrightnessSplitZeroth +from .adapt_split_zeroth import AdaptSplitZeroth from .gaussian_kernel import GaussianKernel from .exponential_kernel import ExponentialKernel from .matern_kernel import MaternKernel -from .matern_adaptive_brightness_kernel import MaternAdaptiveBrightnessKernel +from .matern_adapt_kernel import MaternAdaptKernel diff --git a/autoarray/inversion/regularization/abstract.py b/autoarray/inversion/regularization/abstract.py index 02f5b8511..71a03e1c9 100644 --- a/autoarray/inversion/regularization/abstract.py +++ b/autoarray/inversion/regularization/abstract.py @@ -140,7 +140,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- diff --git a/autoarray/inversion/regularization/adaptive_brightness.py b/autoarray/inversion/regularization/adapt.py similarity index 92% rename from autoarray/inversion/regularization/adaptive_brightness.py rename to autoarray/inversion/regularization/adapt.py index 089229e3a..2c3b03b2d 100644 --- a/autoarray/inversion/regularization/adaptive_brightness.py +++ b/autoarray/inversion/regularization/adapt.py @@ -8,11 +8,11 @@ from autoarray.inversion.regularization.abstract import AbstractRegularization -def adaptive_regularization_weights_from( +def adapt_regularization_weights_from( inner_coefficient: float, outer_coefficient: float, pixel_signals: np.ndarray ) -> np.ndarray: """ - Returns the regularization weights for the adaptive regularization scheme (e.g. ``AdaptiveBrightness``). + Returns the regularization weights for the adaptive regularization scheme (e.g. ``Adapt``). The weights define the effective regularization coefficient of every mesh parameter (typically pixels of a ``Mapper``). @@ -53,10 +53,10 @@ def weighted_regularization_matrix_from( xp=np, ) -> np.ndarray: """ - Returns the regularization matrix of the adaptive regularization scheme (e.g. ``AdaptiveBrightness``). + Returns the regularization matrix of the adaptive regularization scheme (e.g. ``Adapt``). This matrix is computed using the regularization weights of every mesh pixel, which are computed using the - function ``adaptive_regularization_weights_from``. These act as the effective regularization coefficients of + function ``adapt_regularization_weights_from``. These act as the effective regularization coefficients of every mesh pixel. The regularization matrix is computed using the pixel-neighbors array, which is setup using the appropriate @@ -128,7 +128,7 @@ def weighted_regularization_matrix_from( return mat[:S, :S] -class AdaptiveBrightness(AbstractRegularization): +class Adapt(AbstractRegularization): def __init__( self, inner_coefficient: float = 1.0, @@ -196,7 +196,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- @@ -211,7 +211,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra signal_scale=self.signal_scale, xp=xp ) - return adaptive_regularization_weights_from( + return adapt_regularization_weights_from( inner_coefficient=self.inner_coefficient, outer_coefficient=self.outer_coefficient, pixel_signals=pixel_signals, @@ -236,6 +236,6 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray return weighted_regularization_matrix_from( regularization_weights=regularization_weights, - neighbors=linear_obj.source_plane_mesh_grid.neighbors, + neighbors=linear_obj.mesh_geometry.neighbors, xp=xp, ) diff --git a/autoarray/inversion/regularization/adaptive_brightness_split.py b/autoarray/inversion/regularization/adapt_split.py similarity index 88% rename from autoarray/inversion/regularization/adaptive_brightness_split.py rename to autoarray/inversion/regularization/adapt_split.py index 1e77762e1..1afbb4d70 100644 --- a/autoarray/inversion/regularization/adaptive_brightness_split.py +++ b/autoarray/inversion/regularization/adapt_split.py @@ -5,12 +5,12 @@ if TYPE_CHECKING: from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.regularization.adaptive_brightness import AdaptiveBrightness +from autoarray.inversion.regularization.adapt import Adapt from autoarray.inversion.regularization import regularization_util -class AdaptiveBrightnessSplit(AdaptiveBrightness): +class AdaptSplit(Adapt): def __init__( self, inner_coefficient: float = 1.0, @@ -94,16 +94,16 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray linear_obj=linear_obj, xp=xp ) - pix_sub_weights_split_points = linear_obj.pix_sub_weights_split_points + mappings, sizes, weights = linear_obj.interpolator._mappings_sizes_weights_split ( splitted_mappings, splitted_sizes, splitted_weights, ) = regularization_util.reg_split_from( - splitted_mappings=pix_sub_weights_split_points.mappings, - splitted_sizes=pix_sub_weights_split_points.sizes, - splitted_weights=pix_sub_weights_split_points.weights, + splitted_mappings=mappings, + splitted_sizes=sizes, + splitted_weights=weights, xp=xp, ) diff --git a/autoarray/inversion/regularization/adaptive_brightness_split_zeroth.py b/autoarray/inversion/regularization/adapt_split_zeroth.py similarity index 89% rename from autoarray/inversion/regularization/adaptive_brightness_split_zeroth.py rename to autoarray/inversion/regularization/adapt_split_zeroth.py index e4deb290b..4c309396c 100644 --- a/autoarray/inversion/regularization/adaptive_brightness_split_zeroth.py +++ b/autoarray/inversion/regularization/adapt_split_zeroth.py @@ -5,12 +5,12 @@ if TYPE_CHECKING: from autoarray.inversion.linear_obj.linear_obj import LinearObj -from autoarray.inversion.regularization.adaptive_brightness import AdaptiveBrightness +from autoarray.inversion.regularization.adapt import Adapt from autoarray.inversion.regularization.brightness_zeroth import BrightnessZeroth from autoarray.inversion.regularization import regularization_util -class AdaptiveBrightnessSplitZeroth(AdaptiveBrightness): +class AdaptSplitZeroth(Adapt): def __init__( self, zeroth_coefficient: float = 1.0, @@ -96,16 +96,16 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray linear_obj=linear_obj, xp=xp ) - pix_sub_weights_split_points = linear_obj.pix_sub_weights_split_points + mappings, sizes, weights = linear_obj.interpolator._mappings_sizes_weights_split ( splitted_mappings, splitted_sizes, splitted_weights, ) = regularization_util.reg_split_from( - splitted_mappings=pix_sub_weights_split_points.mappings, - splitted_sizes=pix_sub_weights_split_points.sizes, - splitted_weights=pix_sub_weights_split_points.weights, + splitted_mappings=mappings, + splitted_sizes=sizes, + splitted_weights=weights, xp=xp, ) diff --git a/autoarray/inversion/regularization/constant.py b/autoarray/inversion/regularization/constant.py index f8dcc77bc..2625154d7 100644 --- a/autoarray/inversion/regularization/constant.py +++ b/autoarray/inversion/regularization/constant.py @@ -104,7 +104,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- diff --git a/autoarray/inversion/regularization/constant_split.py b/autoarray/inversion/regularization/constant_split.py index 03cd5c9d4..cf63a29a7 100644 --- a/autoarray/inversion/regularization/constant_split.py +++ b/autoarray/inversion/regularization/constant_split.py @@ -56,16 +56,17 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray ------- The regularization matrix. """ - pix_sub_weights_split_points = linear_obj.pix_sub_weights_split_points + + mappings, sizes, weights = linear_obj.interpolator._mappings_sizes_weights_split ( splitted_mappings, splitted_sizes, splitted_weights, ) = regularization_util.reg_split_from( - splitted_mappings=pix_sub_weights_split_points.mappings, - splitted_sizes=pix_sub_weights_split_points.sizes, - splitted_weights=pix_sub_weights_split_points.weights, + splitted_mappings=mappings, + splitted_sizes=sizes, + splitted_weights=weights, xp=xp, ) diff --git a/autoarray/inversion/regularization/constant_zeroth.py b/autoarray/inversion/regularization/constant_zeroth.py index 4d58a8534..3cc14b28d 100644 --- a/autoarray/inversion/regularization/constant_zeroth.py +++ b/autoarray/inversion/regularization/constant_zeroth.py @@ -87,7 +87,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- diff --git a/autoarray/inversion/regularization/exponential_kernel.py b/autoarray/inversion/regularization/exponential_kernel.py index bd188dd5f..0261fc253 100644 --- a/autoarray/inversion/regularization/exponential_kernel.py +++ b/autoarray/inversion/regularization/exponential_kernel.py @@ -84,7 +84,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- diff --git a/autoarray/inversion/regularization/gaussian_kernel.py b/autoarray/inversion/regularization/gaussian_kernel.py index 8dc2574b1..b44900c5b 100644 --- a/autoarray/inversion/regularization/gaussian_kernel.py +++ b/autoarray/inversion/regularization/gaussian_kernel.py @@ -83,7 +83,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- diff --git a/autoarray/inversion/regularization/matern_adaptive_brightness_kernel.py b/autoarray/inversion/regularization/matern_adapt_kernel.py similarity index 51% rename from autoarray/inversion/regularization/matern_adaptive_brightness_kernel.py rename to autoarray/inversion/regularization/matern_adapt_kernel.py index 6442d0343..6e4cc0f87 100644 --- a/autoarray/inversion/regularization/matern_adaptive_brightness_kernel.py +++ b/autoarray/inversion/regularization/matern_adapt_kernel.py @@ -8,78 +8,19 @@ from autoarray.inversion.linear_obj.linear_obj import LinearObj from autoarray.inversion.regularization.matern_kernel import matern_kernel +from autoarray.inversion.regularization.matern_kernel import matern_cov_matrix_from +from autoarray.inversion.regularization.matern_kernel import inv_via_cholesky +from autoarray.inversion.regularization.adapt import adapt_regularization_weights_from -def matern_cov_matrix_from( - scale: float, - nu: float, - pixel_points, - weights=None, - xp=np, -): - """ - Construct the regularization covariance matrix (N x N) using a Matérn kernel, - optionally modulated by per-pixel weights. - - If `weights` is provided (shape [N]), the covariance is: - C_ij = K(d_ij; scale, nu) * w_i * w_j - with a small diagonal jitter added for numerical stability. - - Parameters - ---------- - scale - Typical correlation length of the Matérn kernel. - nu - Smoothness parameter of the Matérn kernel. - pixel_points - Array-like of shape [N, 2] with (y, x) coordinates (or any 2D coords; only distances matter). - weights - Optional array-like of shape [N]. If None, treated as all ones. - xp - Backend (numpy or jax.numpy). - - Returns - ------- - covariance_matrix - Array of shape [N, N]. - """ - - # -------------------------------- - # Pairwise distances (broadcasted) - # -------------------------------- - diff = pixel_points[:, None, :] - pixel_points[None, :, :] # (N, N, 2) - d_ij = xp.sqrt(diff[..., 0] ** 2 + diff[..., 1] ** 2) # (N, N) - - # -------------------------------- - # Base Matérn covariance - # -------------------------------- - covariance_matrix = matern_kernel(d_ij, l=scale, v=nu, xp=xp) # (N, N) - - # -------------------------------- - # Apply weights: C_ij *= w_i * w_j - # (broadcasted outer product, JAX-safe) - # -------------------------------- - if weights is not None: - w = xp.asarray(weights) - # Ensure shape (N,) -> outer product (N,1)*(1,N) -> (N,N) - covariance_matrix = covariance_matrix * (w[:, None] * w[None, :]) - - # -------------------------------- - # Add diagonal jitter (JAX-safe) - # -------------------------------- - pixels = pixel_points.shape[0] - covariance_matrix = covariance_matrix + 1e-8 * xp.eye(pixels) - - return covariance_matrix - - -class MaternAdaptiveBrightnessKernel(MaternKernel): +class MaternAdaptKernel(MaternKernel): def __init__( self, - coefficient: float = 1.0, scale: float = 1.0, nu: float = 0.5, - rho: float = 1.0, + inner_coefficient: float = 1.0, + outer_coefficient: float = 1.0, + signal_scale: float = 1.0, ): """ Regularization which uses a Matern smoothing kernel to regularize the solution with regularization weights @@ -117,29 +58,45 @@ def __init__( receive significantly higher weights (and faint pixels lower weights), while smaller values produce a more uniform weighting. Typical values are of order unity (e.g. 0.5–2.0). """ - super().__init__(coefficient=coefficient, scale=scale, nu=nu) - self.rho = rho + super().__init__(coefficient=0.0, scale=scale, nu=nu) + self.inner_coefficient = inner_coefficient + self.outer_coefficient = outer_coefficient + self.signal_scale = signal_scale - def covariance_kernel_weights_from( - self, linear_obj: LinearObj, xp=np - ) -> np.ndarray: - """ - Returns per-pixel kernel weights that adapt to the reconstructed pixel brightness. + def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray: """ - # Assumes linear_obj.pixel_signals_from is xp-aware elsewhere in the codebase. - pixel_signals = linear_obj.pixel_signals_from(signal_scale=1.0, xp=xp) + Returns the regularization weights of this regularization scheme. + + The regularization weights define the level of regularization applied to each parameter in the linear object + (e.g. the ``pixels`` in a ``Mapper``). + + For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. + + Parameters + ---------- + linear_obj + The linear object (e.g. a ``Mapper``) which uses these weights when performing regularization. - max_signal = xp.max(pixel_signals) - max_signal = xp.maximum(max_signal, 1e-8) # avoid divide-by-zero (JAX-safe) + Returns + ------- + The regularization weights. + """ + pixel_signals = linear_obj.pixel_signals_from( + signal_scale=self.signal_scale, xp=xp + ) - return xp.exp(-self.rho * (1.0 - pixel_signals / max_signal)) + return adapt_regularization_weights_from( + inner_coefficient=self.inner_coefficient, + outer_coefficient=self.outer_coefficient, + pixel_signals=pixel_signals, + ) def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray: - kernel_weights = self.covariance_kernel_weights_from( + kernel_weights = 1.0 / self.regularization_weights_from( linear_obj=linear_obj, xp=xp ) - # Follow the xp pattern used in the Matérn kernel module (often `.array` for grids). pixel_points = linear_obj.source_plane_mesh_grid.array covariance_matrix = matern_cov_matrix_from( @@ -150,10 +107,4 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray xp=xp, ) - return self.coefficient * xp.linalg.inv(covariance_matrix) - - def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray: - """ - Returns the regularization weights of this regularization scheme. - """ - return 1.0 / self.covariance_kernel_weights_from(linear_obj=linear_obj, xp=xp) + return inv_via_cholesky(covariance_matrix, xp=xp) diff --git a/autoarray/inversion/regularization/matern_kernel.py b/autoarray/inversion/regularization/matern_kernel.py index 3e38757ec..b71c077b5 100644 --- a/autoarray/inversion/regularization/matern_kernel.py +++ b/autoarray/inversion/regularization/matern_kernel.py @@ -80,53 +80,88 @@ def matern_cov_matrix_from( scale: float, nu: float, pixel_points, + weights=None, xp=np, ): """ - Consutruct the regularization covariance matrix, which is used to determined the regularization pattern (i.e, - how the different pixels are correlated). + Construct the regularization covariance matrix (N x N) using a Matérn kernel, + optionally modulated by per-pixel weights. - the covariance matrix includes two non-linear parameters, the scale coefficient, which is used to determine - the typical scale of the regularization pattern. The smoothness order parameters mu, whose value determie - the inversion solution is mu-th differentiable. + If `weights` is provided (shape [N]), the covariance is: + C_ij = K(d_ij; scale, nu) * w_i * w_j + with a small diagonal jitter added for numerical stability. Parameters ---------- scale - the typical scale of the regularization pattern . + Typical correlation length of the Matérn kernel. + nu + Smoothness parameter of the Matérn kernel. pixel_points - An 2d array with shape [N_source_pixels, 2], which save the source pixelization coordinates (on source plane). - Something like [[y1,x1], [y2,x2], ...] + Array-like of shape [N, 2] with (y, x) coordinates (or any 2D coords; only distances matter). + weights + Optional array-like of shape [N]. If None, treated as all ones. + xp + Backend (numpy or jax.numpy). Returns ------- - np.ndarray - The source covariance matrix (2d array), shape [N_source_pixels, N_source_pixels]. + covariance_matrix + Array of shape [N, N]. """ # -------------------------------- - # Pairwise distances (broadcasted) + # Pairwise distances WITHOUT (N,N,2) diff # -------------------------------- - # pixel_points[:, None, :] -> (N, 1, 2) - # pixel_points[None, :, :] -> (1, N, 2) - diff = pixel_points[:, None, :] - pixel_points[None, :, :] # (N, N, 2) + pts = xp.asarray(pixel_points) - d_ij = xp.sqrt(diff[..., 0] ** 2 + diff[..., 1] ** 2) # (N, N) + # ||x - y||^2 = ||x||^2 + ||y||^2 - 2 x·y + x2 = xp.sum(pts * pts, axis=1, keepdims=True) # (N, 1) + dist_sq = x2 + x2.T - 2.0 * (pts @ pts.T) # (N, N) + dist_sq = xp.maximum(dist_sq, 0.0) # numerical safety + + d_ij = xp.sqrt(dist_sq + 1e-20) # (N, N) # -------------------------------- - # Apply Matérn kernel elementwise + # Base Matérn covariance # -------------------------------- - covariance_matrix = matern_kernel(d_ij, l=scale, v=nu, xp=xp) + covariance_matrix = matern_kernel(d_ij, l=scale, v=nu, xp=xp) # (N, N) + + # -------------------------------- + # Apply weights: C_ij *= w_i * w_j + # -------------------------------- + if weights is not None: + w = xp.asarray(weights) + covariance_matrix = covariance_matrix * (w[:, None] * w[None, :]) # -------------------------------- # Add diagonal jitter (JAX-safe) # -------------------------------- - pixels = pixel_points.shape[0] - covariance_matrix = covariance_matrix + 1e-8 * xp.eye(pixels) + pixels = pts.shape[0] + covariance_matrix = covariance_matrix + 1e-8 * xp.eye( + pixels, dtype=covariance_matrix.dtype + ) return covariance_matrix +def inv_via_cholesky(C, xp=np): + # NumPy + if xp is np: + import scipy.linalg as la + + cho = la.cho_factor(C, lower=True, check_finite=False) + I = np.eye(C.shape[0], dtype=C.dtype) + return la.cho_solve(cho, I, check_finite=False) + + # JAX + import jax.scipy.linalg as jla + + L = xp.linalg.cholesky(C) + I = xp.eye(C.shape[0], dtype=C.dtype) + return jla.cho_solve((L, True), I) + + class MaternKernel(AbstractRegularization): def __init__(self, coefficient: float = 1.0, scale: float = 1.0, nu: float = 0.5): """ @@ -167,7 +202,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- @@ -200,4 +235,4 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray xp=xp, ) - return self.coefficient * xp.linalg.inv(covariance_matrix) + return self.coefficient * inv_via_cholesky(covariance_matrix, xp=xp) diff --git a/autoarray/inversion/regularization/regularization_util.py b/autoarray/inversion/regularization/regularization_util.py index e0e266f28..abab7a4d6 100644 --- a/autoarray/inversion/regularization/regularization_util.py +++ b/autoarray/inversion/regularization/regularization_util.py @@ -3,10 +3,10 @@ from autoarray import exc -from autoarray.inversion.regularization.adaptive_brightness import ( - adaptive_regularization_weights_from, +from autoarray.inversion.regularization.adapt import ( + adapt_regularization_weights_from, ) -from autoarray.inversion.regularization.adaptive_brightness import ( +from autoarray.inversion.regularization.adapt import ( weighted_regularization_matrix_from, ) from autoarray.inversion.regularization.brightness_zeroth import ( @@ -27,6 +27,60 @@ from autoarray.inversion.regularization.zeroth import zeroth_regularization_matrix_from +def split_points_from(points, area_weights, xp=np): + """ + points : (N, 2) + areas : (N,) + xp : np or jnp + + Returns (4*N, 2) + """ + + N = points.shape[0] + offsets = area_weights + + x = points[:, 0] + y = points[:, 1] + + # Allocate output (N, 4, 2) + out = xp.zeros((N, 4, 2), dtype=points.dtype) + + if xp.__name__.startswith("jax"): + # ---------------------------- + # JAX → use .at[] updates + # ---------------------------- + out = out.at[:, 0, 0].set(x + offsets) + out = out.at[:, 0, 1].set(y) + + out = out.at[:, 1, 0].set(x - offsets) + out = out.at[:, 1, 1].set(y) + + out = out.at[:, 2, 0].set(x) + out = out.at[:, 2, 1].set(y + offsets) + + out = out.at[:, 3, 0].set(x) + out = out.at[:, 3, 1].set(y - offsets) + + else: + + # ---------------------------- + # NumPy → direct assignment OK + # ---------------------------- + out[:, 0, 0] = x + offsets + out[:, 0, 1] = y + + out[:, 1, 0] = x - offsets + out[:, 1, 1] = y + + out[:, 2, 0] = x + out[:, 2, 1] = y + offsets + + out[:, 3, 0] = x + out[:, 3, 1] = y - offsets + + return out.reshape((N * 4, 2)) + + def reg_split_np_from( splitted_mappings: np.ndarray, splitted_sizes: np.ndarray, @@ -62,7 +116,7 @@ def reg_split_np_from( ------- """ - splitted_weights *= -1.0 + splitted_weights = -1.0 * splitted_weights for i in range(len(splitted_mappings)): diff --git a/autoarray/inversion/regularization/zeroth.py b/autoarray/inversion/regularization/zeroth.py index 73e73d7a7..2119e4bd6 100644 --- a/autoarray/inversion/regularization/zeroth.py +++ b/autoarray/inversion/regularization/zeroth.py @@ -78,7 +78,7 @@ def regularization_weights_from(self, linear_obj: LinearObj, xp=np) -> np.ndarra (e.g. the ``pixels`` in a ``Mapper``). For standard regularization (e.g. ``Constant``) are weights are equal, however for adaptive schemes - (e.g. ``AdaptiveBrightness``) they vary to adapt to the data being reconstructed. + (e.g. ``Adapt``) they vary to adapt to the data being reconstructed. Parameters ---------- diff --git a/autoarray/mock.py b/autoarray/mock.py index 86bf91aec..44220ae63 100644 --- a/autoarray/mock.py +++ b/autoarray/mock.py @@ -4,6 +4,7 @@ from autoarray.inversion.mock.mock_regularization import MockRegularization from autoarray.inversion.mock.mock_pixelization import MockPixelization from autoarray.inversion.mock.mock_mapper import MockMapper +from autoarray.inversion.mock.mock_interpolator import MockInterpolator from autoarray.inversion.mock.mock_linear_obj import MockLinearObj from autoarray.inversion.mock.mock_linear_obj_func_list import MockLinearObjFuncList from autoarray.inversion.mock.mock_inversion import MockInversion diff --git a/autoarray/plot/mat_plot/two_d.py b/autoarray/plot/mat_plot/two_d.py index 2bf1cc8a7..d38b8746b 100644 --- a/autoarray/plot/mat_plot/two_d.py +++ b/autoarray/plot/mat_plot/two_d.py @@ -4,10 +4,16 @@ from autoconf import conf -from autoarray.inversion.pixelization.mappers.rectangular import ( - MapperRectangular, +from autoarray.inversion.mesh.interpolator.rectangular import ( + InterpolatorRectangular, +) +from autoarray.inversion.mesh.interpolator.rectangular_uniform import ( + InterpolatorRectangularUniform, +) +from autoarray.inversion.mesh.interpolator.delaunay import InterpolatorDelaunay +from autoarray.inversion.mesh.interpolator.knn import ( + InterpolatorKNearestNeighbor, ) -from autoarray.inversion.pixelization.mappers.delaunay import MapperDelaunay from autoarray.mask.derive.zoom_2d import Zoom2D from autoarray.plot.mat_plot.abstract import AbstractMatPlot from autoarray.plot.auto_labels import AutoLabels @@ -483,13 +489,15 @@ def plot_grid( def plot_mapper( self, - mapper: MapperRectangular, + mapper, visuals_2d: Visuals2D, auto_labels: AutoLabels, pixel_values: np.ndarray = Optional[None], zoom_to_brightest: bool = True, ): - if isinstance(mapper, MapperRectangular): + if isinstance(mapper.interpolator, InterpolatorRectangular) or isinstance( + mapper.interpolator, InterpolatorRectangularUniform + ): self._plot_rectangular_mapper( mapper=mapper, visuals_2d=visuals_2d, @@ -498,7 +506,9 @@ def plot_mapper( zoom_to_brightest=zoom_to_brightest, ) - elif isinstance(mapper, MapperDelaunay): + elif isinstance(mapper.interpolator, InterpolatorDelaunay) or isinstance( + mapper.interpolator, InterpolatorKNearestNeighbor + ): self._plot_delaunay_mapper( mapper=mapper, visuals_2d=visuals_2d, @@ -509,7 +519,7 @@ def plot_mapper( def _plot_rectangular_mapper( self, - mapper: MapperRectangular, + mapper, visuals_2d: Visuals2D, auto_labels: AutoLabels, pixel_values: np.ndarray = Optional[None], @@ -518,15 +528,13 @@ def _plot_rectangular_mapper( if pixel_values is not None: solution_array_2d = array_2d_util.array_2d_native_from( array_2d_slim=pixel_values, - mask_2d=np.full( - fill_value=False, shape=mapper.source_plane_mesh_grid.shape_native - ), + mask_2d=np.full(fill_value=False, shape=mapper.mesh_geometry.shape), ) pixel_values = Array2D.no_mask( values=solution_array_2d, - pixel_scales=mapper.source_plane_mesh_grid.pixel_scales, - origin=mapper.source_plane_mesh_grid.origin, + pixel_scales=mapper.mesh_geometry.pixel_scales, + origin=mapper.mesh_geometry.origin, ) extent = self.axis.config_dict.get("extent") @@ -542,18 +550,18 @@ def _plot_rectangular_mapper( else: ax = self.setup_subplot(aspect=aspect_inv) - shape_native = mapper.source_plane_mesh_grid.shape_native + shape_native = mapper.mesh_geometry.shape if pixel_values is not None: - from autoarray.inversion.pixelization.mappers.rectangular_uniform import ( - MapperRectangularUniform, + from autoarray.inversion.mesh.interpolator.rectangular_uniform import ( + InterpolatorRectangularUniform, ) - from autoarray.inversion.pixelization.mappers.rectangular import ( - MapperRectangular, + from autoarray.inversion.mesh.interpolator.rectangular import ( + InterpolatorRectangular, ) - if isinstance(mapper, MapperRectangularUniform): + if isinstance(mapper.interpolator, InterpolatorRectangularUniform): self.plot_array( array=pixel_values, @@ -645,7 +653,7 @@ def _plot_rectangular_mapper( def _plot_delaunay_mapper( self, - mapper: MapperDelaunay, + mapper, visuals_2d: Visuals2D, auto_labels: AutoLabels, pixel_values: np.ndarray = Optional[None], diff --git a/autoarray/plot/wrap/two_d/delaunay_drawer.py b/autoarray/plot/wrap/two_d/delaunay_drawer.py index 05e2e2126..99f434119 100644 --- a/autoarray/plot/wrap/two_d/delaunay_drawer.py +++ b/autoarray/plot/wrap/two_d/delaunay_drawer.py @@ -2,7 +2,6 @@ import numpy as np from typing import Optional -from autoarray.inversion.pixelization.mappers.delaunay import MapperDelaunay from autoarray.plot.wrap.two_d.abstract import AbstractMatWrap2D from autoarray.plot.wrap.base.units import Units @@ -31,7 +30,7 @@ class DelaunayDrawer(AbstractMatWrap2D): def draw_delaunay_pixels( self, - mapper: MapperDelaunay, + mapper, pixel_values: Optional[np.ndarray], units: Units, cmap: Optional[wb.Cmap], @@ -73,9 +72,9 @@ def draw_delaunay_pixels( if ax is None: ax = plt.gca() - source_pixelization_grid = mapper.mapper_grids.source_plane_mesh_grid + source_pixelization_grid = mapper.source_plane_mesh_grid - simplices = mapper.delaunay.simplices + simplices = mapper.interpolator.delaunay.simplices # Remove padded -1 values required for JAX simplices = np.asarray(simplices) diff --git a/autoarray/preloads.py b/autoarray/preloads.py index 1b4e00d28..c72ad7323 100644 --- a/autoarray/preloads.py +++ b/autoarray/preloads.py @@ -24,9 +24,6 @@ def __init__( source_pixel_zeroed_indices: np.ndarray = None, image_plane_mesh_grid_list: np.ndarray = None, linear_light_profile_blurred_mapping_matrix=None, - use_voronoi_areas: bool = True, - areas_factor: float = 0.5, - skip_areas: bool = False, ): """ Stores preloaded arrays and matrices used during pixelized linear inversions, improving both performance @@ -50,7 +47,7 @@ def __init__( This function iterates over all galaxies with pixelizations, determines which pixelizations have an `image_mesh` and for these pixelizations computes the image-plane mesh-grid. - It returns a list of all image-plane mesh-grids, which in the functions `mapper_from` and `mapper_galaxy_dict` + It returns a list of all image-plane mesh-grids, which in the functions `interpolator_from` and `mapper_galaxy_dict` are grouped into a `Mapper` object with other information required to perform the inversion using the pixelization. @@ -82,16 +79,6 @@ def __init__( inversion, with the other component being the pixelization's pixels. These are fixed when the lens light is fixed to the maximum likelihood solution, allowing the blurred mapping matrix to be preloaded, but the intensity values will still be solved for during the inversion. - use_voronoi_areas - Whether to use Voronoi areas during Delaunay triangulation. When True, computes areas for each Voronoi - region which can be used in certain regularization schemes. Default is True. - areas_factor - Factor used to scale the Voronoi areas during split point computation. Default is 0.5. - skip_areas - Whether to skip Voronoi area calculations and split point computations during Delaunay triangulation. - When True, the Delaunay interface returns only the minimal set of outputs (points, simplices, mappings) - without computing split_points or splitted_mappings. This optimization is useful for regularization - schemes like Matérn kernels that don't require area-based calculations. Default is False. """ self.mapper_indices = None self.source_pixel_zeroed_indices = None @@ -131,7 +118,3 @@ def __init__( self.linear_light_profile_blurred_mapping_matrix = np.array( linear_light_profile_blurred_mapping_matrix ) - - self.use_voronoi_areas = use_voronoi_areas - self.areas_factor = areas_factor - self.skip_areas = skip_areas diff --git a/autoarray/inversion/inversion/settings.py b/autoarray/settings.py similarity index 73% rename from autoarray/inversion/inversion/settings.py rename to autoarray/settings.py index 2324c9498..8f33c99af 100644 --- a/autoarray/inversion/inversion/settings.py +++ b/autoarray/settings.py @@ -7,16 +7,13 @@ logger = logging.getLogger(__name__) -class SettingsInversion: +class Settings: def __init__( self, use_mixed_precision: bool = False, use_positive_only_solver: Optional[bool] = None, - positive_only_uses_p_initial: Optional[bool] = None, use_border_relocator: Optional[bool] = None, no_regularization_add_to_curvature_diag_value: float = None, - tolerance: float = 1e-8, - maxiter: int = 250, ): """ The settings of an Inversion, customizing how a linear set of equations are solved for. @@ -41,24 +38,14 @@ def __init__( no_regularization_add_to_curvature_diag_value If a linear func object does not have a corresponding regularization, this value is added to its diagonal entries of the curvature regularization matrix to ensure the matrix is positive-definite. - tolerance - For an interferometer inversion using the linear operators method, sets the tolerance of the solver - (this input does nothing for dataset data and other interferometer methods). - maxiter - For an interferometer inversion using the linear operators method, sets the maximum number of iterations - of the solver (this input does nothing for dataset data and other interferometer methods). """ self.use_mixed_precision = use_mixed_precision self._use_positive_only_solver = use_positive_only_solver - self._positive_only_uses_p_initial = positive_only_uses_p_initial self._use_border_relocator = use_border_relocator self._no_regularization_add_to_curvature_diag_value = ( no_regularization_add_to_curvature_diag_value ) - self.tolerance = tolerance - self.maxiter = maxiter - @property def use_positive_only_solver(self): if self._use_positive_only_solver is None: @@ -66,13 +53,6 @@ def use_positive_only_solver(self): return self._use_positive_only_solver - @property - def positive_only_uses_p_initial(self): - if self._positive_only_uses_p_initial is None: - return conf.instance["general"]["inversion"]["positive_only_uses_p_initial"] - - return self._positive_only_uses_p_initial - @property def use_border_relocator(self): if self._use_border_relocator is None: diff --git a/autoarray/structures/mesh/abstract_2d.py b/autoarray/structures/mesh/abstract_2d.py deleted file mode 100644 index 76dd13b0a..000000000 --- a/autoarray/structures/mesh/abstract_2d.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Optional, Tuple - -from autoarray.structures.abstract_structure import Structure -from autoarray.structures.grids.uniform_2d import Grid2D - - -class Abstract2DMesh(Structure): - - @property - def slim(self) -> "Structure": - raise NotImplementedError() - - @property - def native(self) -> Structure: - raise NotImplementedError() - - @property - def parameters(self) -> int: - return self.pixels - - @property - def pixels(self) -> int: - raise NotImplementedError - - def interpolation_grid_from( - self, - shape_native: Tuple[int, int] = (401, 401), - extent: Optional[Tuple[float, float, float, float]] = None, - ) -> Grid2D: - """ - Returns a 2D grid of (y,x) coordinates on to which a reconstruction from a pixelization (e.g. a `Delaunay`, - `Delaunay`) can be interpolated. - - The interpolation grid is computed from the pixelization's `extent`, which describes the [x0, x1, y0, y1] - extent that the pixelization covers. This `extent` is converted to an `extent_square` such - that `x1 - x0 = y1 - y1`, ensuring that the interpolation grid can have uniform square pixels. - - Parameters - ---------- - shape_native - The (y,x) shape of the interpolation grid. - extent - The (x0, x1, y0, y1) extent of the grid in scaled coordinates over which the grid is created if it - is input. - """ - - extent = self.geometry.extent_square if extent is None else extent - - return Grid2D.from_extent(extent=extent, shape_native=shape_native) diff --git a/autoarray/structures/mesh/rectangular_2d.py b/autoarray/structures/mesh/rectangular_2d.py deleted file mode 100644 index d1ec1c0f3..000000000 --- a/autoarray/structures/mesh/rectangular_2d.py +++ /dev/null @@ -1,137 +0,0 @@ -import numpy as np - -from typing import List, Optional, Tuple - -from autoarray import type as ty -from autoarray.inversion.linear_obj.neighbors import Neighbors -from autoarray.mask.mask_2d import Mask2D -from autoarray.structures.arrays.uniform_2d import Array2D - -from autoarray.structures.mesh.abstract_2d import Abstract2DMesh - -from autoarray.inversion.pixelization.mesh import mesh_util -from autoarray.structures.grids import grid_2d_util - - -class Mesh2DRectangular(Abstract2DMesh): - - def __init__( - self, - values: np.ndarray, - shape_native: Tuple[int, int], - pixel_scales: ty.PixelScales, - origin: Tuple[float, float] = (0.0, 0.0), - ): - """ - A grid of (y,x) coordinates which represent a uniform rectangular pixelization. - - A `Mesh2DRectangular` is ordered such pixels begin from the top-row and go rightwards and then downwards. - It is an ndarray of shape [total_pixels, 2], where the first dimension of the ndarray corresponds to the - pixelization's pixel index and second element whether it is a y or x arc-second coordinate. - - For example: - - - grid[3,0] = the y-coordinate of the 4th pixel in the rectangular pixelization. - - grid[6,1] = the x-coordinate of the 7th pixel in the rectangular pixelization. - - This class is used in conjuction with the `inversion/pixelizations` package to create rectangular pixelizations - and mappers that perform an `Inversion`. - - Parameters - ---------- - values - The grid of (y,x) coordinates corresponding to the centres of each pixel in the rectangular pixelization. - shape_native - The 2D dimensions of the rectangular pixelization with shape (y_pixels, x_pixel). - pixel_scales - The (y,x) scaled units to pixel units conversion factors of every pixel. If this is input as a `float`, - it is converted to a (float, float) structure. - origin - The (y,x) origin of the pixelization. - """ - - mask = Mask2D.all_false( - shape_native=shape_native, - pixel_scales=pixel_scales, - origin=origin, - ) - - self.mask = mask - - super().__init__(array=values) - - @classmethod - def overlay_grid( - cls, - shape_native: Tuple[int, int], - grid: np.ndarray, - buffer: float = 1e-8, - xp=np, - ) -> "Mesh2DRectangular": - """ - Creates a `Grid2DRecntagular` by overlaying the rectangular pixelization over an input grid of (y,x) - coordinates. - - This is performed by first computing the minimum and maximum y and x coordinates of the input grid. A - rectangular pixelization with dimensions `shape_native` is then laid over the grid using these coordinates, - such that the extreme edges of this rectangular pixelization overlap these maximum and minimum (y,x) coordinates. - - A a `buffer` can be included which increases the size of the rectangular pixelization, placing additional - spacing beyond these maximum and minimum coordinates. - - Parameters - ---------- - shape_native - The 2D dimensions of the rectangular pixelization with shape (y_pixels, x_pixel). - grid - A grid of (y,x) coordinates which the rectangular pixelization is laid-over. - buffer - The size of the extra spacing placed between the edges of the rectangular pixelization and input grid. - """ - grid = grid.array - - y_min = xp.min(grid[:, 0]) - buffer - y_max = xp.max(grid[:, 0]) + buffer - x_min = xp.min(grid[:, 1]) - buffer - x_max = xp.max(grid[:, 1]) + buffer - - pixel_scales = xp.array( - ( - (y_max - y_min) / shape_native[0], - (x_max - x_min) / shape_native[1], - ) - ) - origin = xp.array(((y_max + y_min) / 2.0, (x_max + x_min) / 2.0)) - - grid_slim = grid_2d_util.grid_2d_slim_via_shape_native_not_mask_from( - shape_native=shape_native, pixel_scales=pixel_scales, origin=origin, xp=xp - ) - - return cls( - values=grid_slim, - shape_native=shape_native, - pixel_scales=pixel_scales, - origin=origin, - ) - - @property - def neighbors(self) -> Neighbors: - """ - A class packing the ndarrays describing the neighbors of every pixel in the rectangular pixelization (see - `Neighbors` for a complete description of the neighboring scheme). - - The neighbors of a rectangular pixelization are computed by exploiting the uniform and symmetric nature of the - rectangular grid, as described in the method `mesh_util.rectangular_neighbors_from`. - """ - neighbors, sizes = mesh_util.rectangular_neighbors_from( - shape_native=self.shape_native - ) - - return Neighbors(arr=neighbors.astype("int"), sizes=sizes.astype("int")) - - @property - def pixels(self) -> int: - """ - The total number of pixels in the rectangular pixelization. - """ - return self.shape_native[0] * self.shape_native[1] diff --git a/autoarray/structures/mesh/rectangular_2d_uniform.py b/autoarray/structures/mesh/rectangular_2d_uniform.py deleted file mode 100644 index 688b7c53b..000000000 --- a/autoarray/structures/mesh/rectangular_2d_uniform.py +++ /dev/null @@ -1,6 +0,0 @@ -from autoarray.structures.mesh.rectangular_2d import Mesh2DRectangular - - -class Mesh2DRectangularUniform(Mesh2DRectangular): - - pass diff --git a/autoarray/structures/mock/mock_grid.py b/autoarray/structures/mock/mock_grid.py index b1639a02c..f5d6034fe 100644 --- a/autoarray/structures/mock/mock_grid.py +++ b/autoarray/structures/mock/mock_grid.py @@ -4,7 +4,7 @@ from autoarray.geometry.abstract_2d import AbstractGeometry2D from autoarray.inversion.linear_obj.neighbors import Neighbors from autoarray.structures.abstract_structure import Structure -from autoarray.structures.mesh.abstract_2d import Abstract2DMesh +from autoarray.inversion.mesh.interpolator.abstract import AbstractInterpolator class MockGeometry(AbstractGeometry2D): @@ -16,7 +16,7 @@ def extent(self) -> Tuple[float, float, float, float]: return self._extent -class MockGrid2DMesh(Abstract2DMesh): +class MockGrid2DMesh(AbstractInterpolator): @property def pixels(self) -> int: raise NotImplementedError() @@ -35,7 +35,7 @@ def __init__( """ A grid of (y,x) coordinates which represent a uniform rectangular pixelization. - A `Mesh2DRectangular` is ordered such pixels begin from the top-row and go rightwards and then downwards. + A `InterpolatorRectangular` is ordered such pixels begin from the top-row and go rightwards and then downwards. It is an ndarray of shape [total_pixels, 2], where the first dimension of the ndarray corresponds to the pixelization's pixel index and second element whether it is a y or x arc-second coordinate. diff --git a/autoarray/util/__init__.py b/autoarray/util/__init__.py index afeb1ccb4..32449961a 100644 --- a/autoarray/util/__init__.py +++ b/autoarray/util/__init__.py @@ -10,9 +10,8 @@ from autoarray.structures.grids import sparse_2d_util as sparse from autoarray.layout import layout_util as layout from autoarray.fit import fit_util as fit -from autoarray.inversion.pixelization.mesh import mesh_util as mesh -from autoarray.inversion.pixelization.mappers import mapper_util as mapper -from autoarray.inversion.pixelization.mappers import mapper_numba_util as mapper_numba +from autoarray.inversion.mappers import mapper_util as mapper +from autoarray.inversion.mappers import mapper_numba_util as mapper_numba from autoarray.inversion.regularization import regularization_util as regularization from autoarray.inversion.inversion import inversion_util as inversion from autoarray.inversion.inversion.imaging import ( diff --git a/test_autoarray/config/general.yaml b/test_autoarray/config/general.yaml index 493b3a92c..5dfe968d2 100644 --- a/test_autoarray/config/general.yaml +++ b/test_autoarray/config/general.yaml @@ -12,7 +12,6 @@ inversion: check_reconstruction: false # If True, the inversion's reconstruction is checked to ensure the solution of a meshs's mapper is not an invalid solution where the values are all the same. use_positive_only_solver: false # If True, inversion's use a positive-only linear algebra solver by default, which is slower but prevents unphysical negative values in the reconstructed solutuion. no_regularization_add_to_curvature_diag_value : 1.0e-8 # The default value added to the curvature matrix's diagonal when regularization is not applied to a linear object, which prevents inversion's failing due to the matrix being singular. - positive_only_uses_p_initial: false # If True, the positive-only solver of an inversion's uses an initial guess of the reconstructed data's values as which values should be positive, speeding up the solver. numba: nopython: true cache: true diff --git a/test_autoarray/conftest.py b/test_autoarray/conftest.py index 81708782c..c35f26bf6 100644 --- a/test_autoarray/conftest.py +++ b/test_autoarray/conftest.py @@ -256,6 +256,11 @@ def make_delaunay_mapper_9_3x3(): return fixtures.make_delaunay_mapper_9_3x3() +@pytest.fixture(name="knn_mapper_9_3x3") +def make_knn_mapper_9_3x3(): + return fixtures.make_knn_mapper_9_3x3() + + @pytest.fixture(name="rectangular_inversion_7x7_3x3") def make_rectangular_inversion_7x7_3x3(): return fixtures.make_rectangular_inversion_7x7_3x3() diff --git a/test_autoarray/inversion/inversion/imaging/test_imaging.py b/test_autoarray/inversion/inversion/imaging/test_imaging.py index 6a9c89446..76afc42ec 100644 --- a/test_autoarray/inversion/inversion/imaging/test_imaging.py +++ b/test_autoarray/inversion/inversion/imaging/test_imaging.py @@ -19,7 +19,7 @@ def test__operated_mapping_matrix_property(psf_3x3, rectangular_mapper_7x7_3x3): inversion = aa.m.MockInversionImaging( - mask=rectangular_mapper_7x7_3x3.mapper_grids.mask, + mask=rectangular_mapper_7x7_3x3.mask, psf=psf_3x3, linear_obj_list=[rectangular_mapper_7x7_3x3], ) @@ -73,7 +73,7 @@ def test__operated_mapping_matrix_property__with_operated_mapping_matrix_overrid ) inversion = aa.m.MockInversionImaging( - mask=rectangular_mapper_7x7_3x3.mapper_grids.mask, + mask=rectangular_mapper_7x7_3x3.mask, psf=psf, linear_obj_list=[rectangular_mapper_7x7_3x3, linear_obj], ) @@ -114,9 +114,7 @@ def test__curvature_matrix(rectangular_mapper_7x7_3x3): inversion = aa.InversionImagingMapping( dataset=dataset, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion( - no_regularization_add_to_curvature_diag_value=False - ), + settings=aa.Settings(no_regularization_add_to_curvature_diag_value=False), ) assert inversion.curvature_matrix[0:2, 0:2] == pytest.approx( @@ -129,9 +127,7 @@ def test__curvature_matrix(rectangular_mapper_7x7_3x3): inversion = aa.InversionImagingMapping( dataset=dataset, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion( - no_regularization_add_to_curvature_diag_value=True - ), + settings=aa.Settings(no_regularization_add_to_curvature_diag_value=True), ) assert inversion.curvature_matrix[0, 0] - 10.0 > 0.0 diff --git a/test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py b/test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py index 3069a890c..235318cb3 100644 --- a/test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py +++ b/test_autoarray/inversion/inversion/imaging/test_inversion_imaging_util.py @@ -198,7 +198,7 @@ def test__data_vector_via_weighted_data_two_methods_agree(): psf = kernel - pixelization = aa.mesh.RectangularUniform(shape=(20, 20)) + mesh = aa.mesh.RectangularUniform(shape=(20, 20)) # TODO : Use pytest.parameterize @@ -206,16 +206,11 @@ def test__data_vector_via_weighted_data_two_methods_agree(): grid = aa.Grid2D.from_mask(mask=mask, over_sample_size=sub_size) - mapper_grids = pixelization.mapper_grids_from( - mask=mask, - border_relocator=None, - source_plane_data_grid=grid, + interpolator = mesh.interpolator_from( + source_plane_data_grid=grid, source_plane_mesh_grid=None ) - mapper = aa.Mapper( - mapper_grids=mapper_grids, - regularization=None, - ) + mapper = aa.Mapper(interpolator=interpolator) mapping_matrix = mapper.mapping_matrix @@ -256,7 +251,7 @@ def test__data_vector_via_weighted_data_two_methods_agree(): rows=rows, cols=cols, vals=vals, - S=pixelization.pixels, + S=mesh.pixels, ) ) @@ -284,13 +279,12 @@ def test__curvature_matrix_via_psf_weighted_noise_two_methods_agree(): mesh = aa.mesh.RectangularAdaptDensity(shape=(20, 20)) - mapper_grids = mesh.mapper_grids_from( - mask=mask, - border_relocator=None, + interpolator = mesh.interpolator_from( source_plane_data_grid=mask.derive_grid.unmasked, + source_plane_mesh_grid=None, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) + mapper = aa.Mapper(interpolator=interpolator) mapping_matrix = mapper.mapping_matrix @@ -326,5 +320,5 @@ def test__curvature_matrix_via_psf_weighted_noise_two_methods_agree(): ) assert curvature_matrix_via_sparse_operator == pytest.approx( - curvature_matrix, rel=1.0e-3 + curvature_matrix, abs=1.0e-4 ) diff --git a/test_autoarray/inversion/inversion/interferometer/test_interferometer.py b/test_autoarray/inversion/inversion/interferometer/test_interferometer.py index 774ea5c0d..dc67de18f 100644 --- a/test_autoarray/inversion/inversion/interferometer/test_interferometer.py +++ b/test_autoarray/inversion/inversion/interferometer/test_interferometer.py @@ -17,9 +17,7 @@ def test__curvature_matrix(rectangular_mapper_7x7_3x3): linear_obj_list=[aa.m.MockLinearObj(parameters=1), rectangular_mapper_7x7_3x3], operated_mapping_matrix=operated_mapping_matrix, noise_map=noise_map, - settings=aa.SettingsInversion( - no_regularization_add_to_curvature_diag_value=False - ), + settings=aa.Settings(no_regularization_add_to_curvature_diag_value=False), ) assert inversion.curvature_matrix[0:2, 0:2] == pytest.approx( @@ -33,9 +31,7 @@ def test__curvature_matrix(rectangular_mapper_7x7_3x3): linear_obj_list=[aa.m.MockLinearObj(parameters=1), rectangular_mapper_7x7_3x3], operated_mapping_matrix=operated_mapping_matrix, noise_map=noise_map, - settings=aa.SettingsInversion( - no_regularization_add_to_curvature_diag_value=True - ), + settings=aa.Settings(no_regularization_add_to_curvature_diag_value=True), ) assert inversion.curvature_matrix[0, 0] - 4.0 > 0.0 @@ -50,7 +46,7 @@ def test__fast_chi_squared( inversion = aa.Inversion( dataset=interferometer_7_no_fft, linear_obj_list=[rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion(), + settings=aa.Settings(), ) residual_map = aa.util.fit.residual_map_from( diff --git a/test_autoarray/inversion/inversion/test_abstract.py b/test_autoarray/inversion/inversion/test_abstract.py index 685fa9239..41bee0416 100644 --- a/test_autoarray/inversion/inversion/test_abstract.py +++ b/test_autoarray/inversion/inversion/test_abstract.py @@ -48,7 +48,7 @@ def test__index_range_list_from(): ) assert inversion.param_range_list_from(cls=aa.LinearObj) == [[0, 2], [2, 3]] - assert inversion.param_range_list_from(cls=aa.AbstractMapper) == [[2, 3]] + assert inversion.param_range_list_from(cls=aa.Mapper) == [[2, 3]] def test__no_regularization_index_list(): @@ -103,24 +103,18 @@ def test__curvature_matrix__via_sparse_operator__identical_to_mapping(): mesh_0 = aa.mesh.RectangularUniform(shape=(3, 3)) mesh_1 = aa.mesh.RectangularUniform(shape=(4, 4)) - mapper_grids_0 = mesh_0.mapper_grids_from( - mask=mask, - border_relocator=None, + interpolator_0 = mesh_0.interpolator_from( source_plane_data_grid=grid, source_plane_mesh_grid=None, ) - mapper_grids_1 = mesh_1.mapper_grids_from( - mask=mask, - border_relocator=None, + interpolator_1 = mesh_1.interpolator_from( source_plane_data_grid=grid, source_plane_mesh_grid=None, ) - reg = aa.reg.Constant(coefficient=1.0) - - mapper_0 = aa.Mapper(mapper_grids=mapper_grids_0, regularization=reg) - mapper_1 = aa.Mapper(mapper_grids=mapper_grids_1, regularization=reg) + mapper_0 = aa.Mapper(interpolator=interpolator_0) + mapper_1 = aa.Mapper(interpolator=interpolator_1) image = aa.Array2D.no_mask(values=np.random.random((7, 7)), pixel_scales=1.0) noise_map = aa.Array2D.no_mask(values=np.random.random((7, 7)), pixel_scales=1.0) @@ -178,24 +172,18 @@ def test__curvature_matrix_via_sparse_operator__includes_source_interpolation__i mask=mask, adapt_data=None ) - mapper_grids_0 = mesh_0.mapper_grids_from( - mask=mask, - border_relocator=None, + interpolator_0 = mesh_0.interpolator_from( source_plane_data_grid=grid, source_plane_mesh_grid=image_mesh_grid_0, ) - mapper_grids_1 = mesh_1.mapper_grids_from( - mask=mask, - border_relocator=None, + interpolator_1 = mesh_1.interpolator_from( source_plane_data_grid=grid, source_plane_mesh_grid=image_mesh_grid_1, ) - reg = aa.reg.Constant(coefficient=1.0) - - mapper_0 = aa.Mapper(mapper_grids=mapper_grids_0, regularization=reg) - mapper_1 = aa.Mapper(mapper_grids=mapper_grids_1, regularization=reg) + mapper_0 = aa.Mapper(interpolator=interpolator_0) + mapper_1 = aa.Mapper(interpolator=interpolator_1) image = aa.Array2D.no_mask(values=np.random.random((7, 7)), pixel_scales=1.0) noise_map = aa.Array2D.no_mask(values=np.random.random((7, 7)), pixel_scales=1.0) @@ -559,10 +547,22 @@ def test__reconstruction_noise_map(): def test__max_pixel_list_from_and_centre(): + + source_plane_mesh_grid = aa.Grid2DIrregular( + [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [5.0, 0.0]] + ) + + mapper = aa.m.MockMapper(source_plane_mesh_grid=source_plane_mesh_grid) + + interpolator = aa.InterpolatorDelaunay( + mesh=aa.mesh.Delaunay(), + mesh_grid=source_plane_mesh_grid, + data_grid=None, + ) + mapper = aa.m.MockMapper( - source_plane_mesh_grid=aa.Mesh2DDelaunay( - [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [5.0, 0.0]] - ) + source_plane_mesh_grid=source_plane_mesh_grid, + interpolator=interpolator, ) inversion = aa.m.MockInversion( @@ -578,21 +578,29 @@ def test__max_pixel_list_from_and_centre(): def test__max_pixel_list_from__filter_neighbors(): + source_plane_mesh_grid = aa.Grid2DIrregular( + [ + [1.0, 1.0], + [1.0, 2.0], + [1.0, 3.0], + [2.0, 1.0], + [2.0, 2.0], + [2.0, 3.0], + [3.0, 1.0], + [3.0, 2.0], + [3.0, 3.0], + ] + ) + + mesh_geometry = aa.MeshGeometryDelaunay( + mesh=aa.mesh.Delaunay(), + mesh_grid=source_plane_mesh_grid, + data_grid=None, + ) mapper = aa.m.MockMapper( - source_plane_mesh_grid=aa.Mesh2DDelaunay( - [ - [1.0, 1.0], - [1.0, 2.0], - [1.0, 3.0], - [2.0, 1.0], - [2.0, 2.0], - [2.0, 3.0], - [3.0, 1.0], - [3.0, 2.0], - [3.0, 3.0], - ] - ) + source_plane_mesh_grid=source_plane_mesh_grid, + mesh_geometry=mesh_geometry, ) inversion = aa.m.MockInversion( diff --git a/test_autoarray/inversion/inversion/test_factory.py b/test_autoarray/inversion/inversion/test_factory.py index a679682a4..a8b62b0f1 100644 --- a/test_autoarray/inversion/inversion/test_factory.py +++ b/test_autoarray/inversion/inversion/test_factory.py @@ -76,7 +76,6 @@ def test__inversion_imaging__via_mapper( linear_obj_list=[rectangular_mapper_7x7_3x3], ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperRectangularUniform) assert isinstance(inversion, aa.InversionImagingMapping) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx( 7.2571757082, 1.0e-4 @@ -97,7 +96,6 @@ def test__inversion_imaging__via_mapper( linear_obj_list=[rectangular_mapper_7x7_3x3], ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperRectangularUniform) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx( 7.257175708246, 1.0e-4 ) @@ -110,7 +108,6 @@ def test__inversion_imaging__via_mapper( linear_obj_list=[delaunay_mapper_9_3x3], ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperDelaunay) assert isinstance(inversion, aa.InversionImagingMapping) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx(10.6674, 1.0e-4) assert inversion.mapped_reconstructed_operated_data == pytest.approx( @@ -122,22 +119,119 @@ def test__inversion_imaging__via_mapper( linear_obj_list=[delaunay_mapper_9_3x3], ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperDelaunay) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx(10.6674, 1.0e-4) assert inversion.mapped_reconstructed_operated_data == pytest.approx( np.ones(9), 1.0e-4 ) +def test__inversion_imaging__via_mapper_knn( + masked_imaging_7x7_no_blur, + knn_mapper_9_3x3, + regularization_adaptive_brightness_split, +): + + inversion = aa.Inversion( + dataset=masked_imaging_7x7_no_blur, + linear_obj_list=[knn_mapper_9_3x3], + ) + + assert knn_mapper_9_3x3.pix_indexes_for_sub_slim_index[0, :] == pytest.approx( + [1, 0, 4, 6, 2, 5, 3, 7, 8], 1.0e-4 + ) + assert knn_mapper_9_3x3.pix_indexes_for_sub_slim_index[1, :] == pytest.approx( + [1, 0, 2, 4, 6, 3, 5, 7, 8], 1.0e-4 + ) + assert knn_mapper_9_3x3.pix_indexes_for_sub_slim_index[2, :] == pytest.approx( + [1, 0, 4, 6, 2, 5, 3, 7, 8], 1.0e-4 + ) + + assert knn_mapper_9_3x3.pix_weights_for_sub_slim_index[0, :] == pytest.approx( + [ + 0.24139248, + 0.20182463, + 0.13465525, + 0.12882639, + 0.12169429, + 0.08682546, + 0.07062276, + 0.00982079, + 0.00433794, + ], + 1.0e-4, + ) + assert knn_mapper_9_3x3.pix_weights_for_sub_slim_index[1, :] == pytest.approx( + [ + 0.23255487, + 0.22727716, + 0.14466056, + 0.11643257, + 0.09868897, + 0.08878719, + 0.07744259, + 0.01010399, + 0.0040521, + ], + 1.0e-4, + ) + assert knn_mapper_9_3x3.pix_weights_for_sub_slim_index[2, :] == pytest.approx( + [ + 0.2334672, + 0.1785593, + 0.153417, + 0.15099354, + 0.11075057, + 0.09986048, + 0.06060822, + 0.00869774, + 0.00364596, + ], + 1.0e-4, + ) + + assert isinstance(inversion, aa.InversionImagingMapping) + + assert inversion.regularization_matrix[0:3, 0] == pytest.approx( + [4.00000001, -1.0, -1.0], 1.0e-4 + ) + assert inversion.regularization_matrix[0:3, 1] == pytest.approx( + [-1.0, 3.00000001, 0.0], 1.0e-4 + ) + assert inversion.regularization_matrix[0:3, 2] == pytest.approx( + [-1.0, 0.0, 4.00000001], 1.0e-4 + ) + + assert inversion.log_det_curvature_reg_matrix_term == pytest.approx( + 10.417803331712355, 1.0e-4 + ) + assert inversion.mapped_reconstructed_operated_data == pytest.approx( + np.ones(9), 1.0e-4 + ) + + mapper = copy.copy(knn_mapper_9_3x3) + mapper.regularization = regularization_adaptive_brightness_split + + inversion = aa.Inversion( + dataset=masked_imaging_7x7_no_blur, + linear_obj_list=[mapper], + ) + + assert inversion.regularization_matrix[0:3, 0] == pytest.approx( + [22.47519068, -16.373819, 8.39424766], 1.0e-4 + ) + assert inversion.regularization_matrix[0:3, 1] == pytest.approx( + [-16.373819, 112.1402519, -13.56808248], 1.0e-4 + ) + assert inversion.regularization_matrix[0:3, 2] == pytest.approx( + [8.39424766, -13.56808248, 26.10743213], 1.0e-4 + ) + + def test__inversion_imaging__via_regularizations( masked_imaging_7x7_no_blur, delaunay_mapper_9_3x3, regularization_constant, - regularization_constant_split, regularization_adaptive_brightness, - regularization_adaptive_brightness_split, - regularization_gaussian_kernel, - regularization_exponential_kernel, ): mapper = copy.copy(delaunay_mapper_9_3x3) mapper.regularization = regularization_constant @@ -151,7 +245,6 @@ def test__inversion_imaging__via_regularizations( linear_obj_list=[mapper], ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperDelaunay) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx( 10.66747, 1.0e-4 ) @@ -167,7 +260,6 @@ def test__inversion_imaging__via_regularizations( linear_obj_list=[mapper], ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperDelaunay) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx( 47.410169, 1.0e-4 ) @@ -183,7 +275,7 @@ def test__inversion_imaging__source_pixel_zeroed_indices( inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion(use_positive_only_solver=True), + settings=aa.Settings(use_positive_only_solver=True), preloads=aa.Preloads( mapper_indices=range(0, 9), source_pixel_zeroed_indices=np.array([0]) ), @@ -213,13 +305,12 @@ def test__inversion_imaging__via_linear_obj_func_and_mapper( inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion( + settings=aa.Settings( no_regularization_add_to_curvature_diag_value=False, ), ) assert isinstance(inversion.linear_obj_list[0], aa.m.MockLinearObj) - assert isinstance(inversion.linear_obj_list[1], aa.MapperRectangularUniform) assert isinstance(inversion, aa.InversionImagingMapping) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx( 7.2571757082469945, 1.0e-4 @@ -239,13 +330,12 @@ def test__inversion_imaging__via_linear_obj_func_and_mapper( inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur_sparse_operator, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion( + settings=aa.Settings( no_regularization_add_to_curvature_diag_value=False, ), ) assert isinstance(inversion.linear_obj_list[0], aa.m.MockLinearObj) - assert isinstance(inversion.linear_obj_list[1], aa.MapperRectangularUniform) assert inversion.log_det_curvature_reg_matrix_term == pytest.approx( 7.2571757082469945, 1.0e-4 ) @@ -270,26 +360,24 @@ def test__inversion_imaging__via_linear_obj_func_and_mapper__force_edge_pixels_t inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[linear_obj, delaunay_mapper_9_3x3], - settings=aa.SettingsInversion( + settings=aa.Settings( no_regularization_add_to_curvature_diag_value=False, ), ) assert isinstance(inversion.linear_obj_list[0], aa.m.MockLinearObj) - assert isinstance(inversion.linear_obj_list[1], aa.MapperDelaunay) assert isinstance(inversion, aa.InversionImagingMapping) inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[linear_obj, delaunay_mapper_9_3x3], - settings=aa.SettingsInversion( + settings=aa.Settings( use_positive_only_solver=True, no_regularization_add_to_curvature_diag_value=False, ), ) assert isinstance(inversion.linear_obj_list[0], aa.m.MockLinearObj) - assert isinstance(inversion.linear_obj_list[1], aa.MapperDelaunay) assert isinstance(inversion, aa.InversionImagingMapping) @@ -307,7 +395,7 @@ def test__inversion_imaging__compare_mapping_and_sparse_operator_values( inversion_mapping = aa.Inversion( dataset=masked_imaging_7x7, linear_obj_list=[delaunay_mapper_9_3x3], - settings=aa.SettingsInversion(), + settings=aa.Settings(), ) assert inversion_sparse_operator.reconstruction == pytest.approx( @@ -344,7 +432,7 @@ def test__inversion_imaging__linear_obj_func_and_non_func_give_same_terms( inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion( + settings=aa.Settings( use_positive_only_solver=True, ), ) @@ -358,7 +446,7 @@ def test__inversion_imaging__linear_obj_func_and_non_func_give_same_terms( inversion_no_linear_func = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion( + settings=aa.Settings( use_positive_only_solver=True, ), ) @@ -395,7 +483,7 @@ def test__inversion_imaging__linear_obj_func_with_sparse_operator( inversion_mapping = aa.Inversion( dataset=masked_imaging_7x7, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion(use_positive_only_solver=True), + settings=aa.Settings(use_positive_only_solver=True), ) masked_imaging_7x7_sparse_operator = masked_imaging_7x7.apply_sparse_operator_cpu() @@ -403,7 +491,7 @@ def test__inversion_imaging__linear_obj_func_with_sparse_operator( inversion_sparse_operator = aa.Inversion( dataset=masked_imaging_7x7_sparse_operator, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion(use_positive_only_solver=True), + settings=aa.Settings(use_positive_only_solver=True), ) assert inversion_mapping.data_vector == pytest.approx( @@ -439,7 +527,7 @@ def test__inversion_imaging__linear_obj_func_with_sparse_operator( linear_obj_1, linear_obj_2, ], - settings=aa.SettingsInversion(), + settings=aa.Settings(), ) masked_imaging_7x7_sparse_operator = masked_imaging_7x7.apply_sparse_operator_cpu() @@ -471,10 +559,9 @@ def test__inversion_interferometer__via_mapper( inversion = aa.Inversion( dataset=interferometer_7_no_fft, linear_obj_list=[rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion(), + settings=aa.Settings(), ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperRectangularUniform) assert isinstance(inversion, aa.InversionInterferometerMapping) assert inversion.mapped_reconstructed_operated_data == pytest.approx( 1.0 + 0.0j * np.ones(shape=(7,)), 1.0e-4 @@ -486,10 +573,9 @@ def test__inversion_interferometer__via_mapper( inversion = aa.Inversion( dataset=interferometer_7_no_fft, linear_obj_list=[delaunay_mapper_9_3x3], - settings=aa.SettingsInversion(), + settings=aa.Settings(), ) - assert isinstance(inversion.linear_obj_list[0], aa.MapperDelaunay) assert isinstance(inversion, aa.InversionInterferometerMapping) assert inversion.mapped_reconstructed_operated_data == pytest.approx( 1.0 + 0.0j * np.ones(shape=(7,)), 1.0e-4 @@ -511,7 +597,7 @@ def test__inversion_matrices__x2_mappers( inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[rectangular_mapper_7x7_3x3, delaunay_mapper_9_3x3], - settings=aa.SettingsInversion(use_positive_only_solver=True), + settings=aa.Settings(use_positive_only_solver=True), ) assert inversion.operated_mapping_matrix[0:9, 0:9] == pytest.approx( @@ -586,7 +672,7 @@ def test__inversion_imaging__positive_only_solver(masked_imaging_7x7_no_blur): inversion = aa.Inversion( dataset=masked_imaging_7x7_no_blur, linear_obj_list=[linear_obj], - settings=aa.SettingsInversion(use_positive_only_solver=True), + settings=aa.Settings(use_positive_only_solver=True), ) assert isinstance(inversion.linear_obj_list[0], aa.m.MockLinearObjFuncList) @@ -617,7 +703,7 @@ def test__data_linear_func_matrix_dict( inversion_mapping = aa.Inversion( dataset=masked_imaging_7x7, linear_obj_list=[linear_obj, rectangular_mapper_7x7_3x3], - settings=aa.SettingsInversion(use_positive_only_solver=True), + settings=aa.Settings(use_positive_only_solver=True), ) assert inversion_mapping.data_linear_func_matrix_dict[linear_obj][ diff --git a/test_autoarray/inversion/inversion/test_settings_dict.py b/test_autoarray/inversion/inversion/test_settings_dict.py index 12cd0f759..0cf2a0661 100644 --- a/test_autoarray/inversion/inversion/test_settings_dict.py +++ b/test_autoarray/inversion/inversion/test_settings_dict.py @@ -10,28 +10,25 @@ @pytest.fixture(name="settings_dict") def make_settings_dict(): return { - "class_path": "autoarray.inversion.inversion.settings.SettingsInversion", + "class_path": "autoarray.settings.Settings", "type": "instance", "arguments": { "use_positive_only_solver": False, - "positive_only_uses_p_initial": False, "no_regularization_add_to_curvature_diag_value": 1e-08, - "tolerance": 1e-08, - "maxiter": 250, }, } def test_settings_from_dict(settings_dict): - assert isinstance(from_dict(settings_dict), aa.SettingsInversion) + assert isinstance(from_dict(settings_dict), aa.Settings) def test_file(): filename = Path("/tmp/temp.json") - output_to_json(aa.SettingsInversion(), filename) + output_to_json(aa.Settings(), filename) try: - assert isinstance(from_json(filename), aa.SettingsInversion) + assert isinstance(from_json(filename), aa.Settings) finally: os.remove(filename) diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_overlay.py b/test_autoarray/inversion/pixelization/image_mesh/test_overlay.py index 54f242236..74098097c 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_overlay.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_overlay.py @@ -2,7 +2,7 @@ import autoarray as aa -from autoarray.inversion.pixelization.image_mesh import overlay as overlay_util +from autoarray.inversion.mesh.image_mesh import overlay as overlay_util def test__total_pixels_2d_from(): diff --git a/test_autoarray/inversion/pixelization/interpolator/__init__.py b/test_autoarray/inversion/pixelization/interpolator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_autoarray/inversion/pixelization/interpolator/test_delaunay.py b/test_autoarray/inversion/pixelization/interpolator/test_delaunay.py new file mode 100644 index 000000000..aaa0e2980 --- /dev/null +++ b/test_autoarray/inversion/pixelization/interpolator/test_delaunay.py @@ -0,0 +1,59 @@ +import numpy as np +import pytest + +import autoarray as aa + + +def test__scipy_delaunay__simplices(grid_2d_sub_1_7x7): + + 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, + over_sample_size=1, + ) + + mesh_grid = aa.InterpolatorDelaunay( + mesh=aa.mesh.Delaunay(), + mesh_grid=mesh_grid, + data_grid=grid_2d_sub_1_7x7, + ) + + assert (mesh_grid.delaunay.simplices[0, :] == np.array([3, 4, 0])).all() + assert (mesh_grid.delaunay.simplices[1, :] == np.array([3, 5, 4])).all() + assert (mesh_grid.delaunay.simplices[-1, :] == np.array([-1, -1, -1])).all() + + +def test__scipy_delaunay__split(grid_2d_sub_1_7x7): + + 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, + over_sample_size=1, + ) + + mesh_grid = aa.InterpolatorDelaunay( + mesh=aa.mesh.Delaunay(), + mesh_grid=mesh_grid, + data_grid=grid_2d_sub_1_7x7, + ) + + assert mesh_grid.delaunay.split_points[0, :] == pytest.approx( + [0.45059473, 0.1], 1.0e-4 + ) + assert mesh_grid.delaunay.split_points[1, :] == pytest.approx( + [-0.25059473, 0.1], 1.0e-4 + ) + assert mesh_grid.delaunay.split_points[-1, :] == pytest.approx( + [2.1, 0.39142161], 1.0e-4 + ) + assert mesh_grid.delaunay.splitted_mappings[0, :] == pytest.approx( + [2, 1, 0], 1.0e-4 + ) + assert mesh_grid.delaunay.splitted_mappings[1, :] == pytest.approx( + [0, -1, -1], 1.0e-4 + ) + assert mesh_grid.delaunay.splitted_mappings[-1, :] == pytest.approx( + [5, 1, 2], 1.0e-4 + ) diff --git a/test_autoarray/inversion/pixelization/interpolator/test_rectangular.py b/test_autoarray/inversion/pixelization/interpolator/test_rectangular.py new file mode 100644 index 000000000..2ae28399f --- /dev/null +++ b/test_autoarray/inversion/pixelization/interpolator/test_rectangular.py @@ -0,0 +1 @@ +pass diff --git a/test_autoarray/inversion/pixelization/mappers/test_abstract.py b/test_autoarray/inversion/pixelization/mappers/test_abstract.py index 7e01cd70c..0b0f258d3 100644 --- a/test_autoarray/inversion/pixelization/mappers/test_abstract.py +++ b/test_autoarray/inversion/pixelization/mappers/test_abstract.py @@ -3,12 +3,10 @@ import autoarray as aa -from autoarray.inversion.pixelization.mappers.abstract import PixSubWeights - def test__pix_indexes_for_slim_indexes__different_types_of_lists_input(): mapper = aa.m.MockMapper( - pix_sub_weights=PixSubWeights( + interpolator=aa.m.MockInterpolator( mappings=np.array([[0], [0], [0], [0], [0], [0], [0], [0]]), sizes=np.ones(8, dtype="int"), weights=np.ones(9), @@ -23,7 +21,7 @@ def test__pix_indexes_for_slim_indexes__different_types_of_lists_input(): assert pixe_indexes_for_slim_indexes == [0, 1, 2, 3, 4, 5, 6, 7] mapper = aa.m.MockMapper( - pix_sub_weights=PixSubWeights( + interpolator=aa.m.MockInterpolator( mappings=np.array([[0], [0], [0], [0], [3], [4], [4], [7]]), sizes=np.ones(8, dtype="int"), weights=np.ones(8), @@ -40,7 +38,7 @@ def test__pix_indexes_for_slim_indexes__different_types_of_lists_input(): def test__sub_slim_indexes_for_pix_index(): mapper = aa.m.MockMapper( - pix_sub_weights=PixSubWeights( + interpolator=aa.m.MockInterpolator( mappings=np.array( [[0, 4], [1, 4], [2, 4], [0, 4], [1, 4], [3, 4], [0, 4], [3, 4]] ).astype("int"), @@ -72,7 +70,7 @@ def test__sub_slim_indexes_for_pix_index(): def test__data_weight_total_for_pix_from(): mapper = aa.m.MockMapper( - pix_sub_weights=PixSubWeights( + interpolator=aa.m.MockInterpolator( mappings=np.array( [[0, 4], [1, 4], [2, 4], [0, 4], [1, 4], [3, 4], [0, 4], [3, 4]] ).astype("int"), @@ -101,7 +99,7 @@ def test__data_weight_total_for_pix_from(): def test__adaptive_pixel_signals_from___matches_util(grid_2d_7x7, image_7x7): pixels = 6 signal_scale = 2.0 - pix_sub_weights = PixSubWeights( + interpolator = aa.m.MockInterpolator( mappings=np.array([[1], [1], [4], [0], [0], [3], [0], [0], [3]]), sizes=np.array([1, 1, 1, 1, 1, 1, 1, 1, 1]), weights=np.ones(9), @@ -113,7 +111,7 @@ def test__adaptive_pixel_signals_from___matches_util(grid_2d_7x7, image_7x7): mapper = aa.m.MockMapper( source_plane_data_grid=grid_2d_7x7, over_sampler=over_sampler, - pix_sub_weights=pix_sub_weights, + interpolator=interpolator, adapt_data=image_7x7, parameters=pixels, ) @@ -124,8 +122,8 @@ def test__adaptive_pixel_signals_from___matches_util(grid_2d_7x7, image_7x7): pixels=pixels, pixel_weights=pix_weights_for_sub_slim_index, signal_scale=signal_scale, - pix_indexes_for_sub_slim_index=pix_sub_weights.mappings, - pix_size_for_sub_slim_index=pix_sub_weights.sizes, + pix_indexes_for_sub_slim_index=interpolator.mappings, + pix_size_for_sub_slim_index=interpolator.sizes, slim_index_for_sub_slim_index=over_sampler.slim_for_sub_slim, adapt_data=np.array(image_7x7), ) @@ -141,17 +139,14 @@ def test__mapped_to_source_from(grid_2d_7x7): over_sample_size=1, ) - mesh_grid = aa.Mesh2DDelaunay( - values=mesh_grid, source_plane_data_grid_over_sampled=grid_2d_7x7.over_sampled - ) + mesh = aa.mesh.Delaunay() - mapper_grids = aa.MapperGrids( - mask=grid_2d_7x7.mask, + interpolator = mesh.interpolator_from( source_plane_data_grid=grid_2d_7x7, source_plane_mesh_grid=mesh_grid, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) + mapper = aa.Mapper(interpolator=interpolator) array_slim = aa.Array2D.no_mask( [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], diff --git a/test_autoarray/inversion/pixelization/mappers/test_delaunay.py b/test_autoarray/inversion/pixelization/mappers/test_delaunay.py index b8ea84965..f67586319 100644 --- a/test_autoarray/inversion/pixelization/mappers/test_delaunay.py +++ b/test_autoarray/inversion/pixelization/mappers/test_delaunay.py @@ -1,14 +1,34 @@ import numpy as np import scipy.spatial -import pytest import autoarray as aa -from autoarray.structures.mesh.delaunay_2d import ( +from autoarray.inversion.mesh.interpolator.delaunay import ( pix_indexes_for_sub_slim_index_delaunay_from, ) +from autoarray.inversion.mesh.interpolator.delaunay import ( + pixel_weights_delaunay_from, +) + + +def test__pixel_weights_delaunay_from(): + data_grid = np.array([[0.1, 0.1], [1.0, 1.0]]) + + mesh_grid = np.array([[0.0, 0.0], [0.1, 0.0], [0.2, 0.0]]) + + pix_indexes_for_sub_slim_index = np.array([[0, 1, 2], [2, -1, -1]]) + + pixel_weights = pixel_weights_delaunay_from( + data_grid=data_grid, + mesh_grid=mesh_grid, + pix_indexes_for_sub_slim_index=pix_indexes_for_sub_slim_index, + ) + + assert (pixel_weights == np.array([[0.25, 0.5, 0.25], [1.0, 0.0, 0.0]])).all() + + def test__pix_indexes_for_sub_slim_index__matches_util(grid_2d_sub_1_7x7): 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]], @@ -17,32 +37,30 @@ def test__pix_indexes_for_sub_slim_index__matches_util(grid_2d_sub_1_7x7): over_sample_size=1, ) - mesh_grid = aa.Mesh2DDelaunay( - values=mesh_grid, - source_plane_data_grid_over_sampled=grid_2d_sub_1_7x7.over_sampled, - _xp=np, - ) + mesh = aa.mesh.Delaunay() - mapper_grids = aa.MapperGrids( - mask=grid_2d_sub_1_7x7.mask, + interpolator = mesh.interpolator_from( source_plane_data_grid=grid_2d_sub_1_7x7, source_plane_mesh_grid=mesh_grid, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) + mapper = aa.Mapper(interpolator=interpolator) - delaunay = scipy.spatial.Delaunay(mesh_grid.mesh_grid_xy) + delaunay = scipy.spatial.Delaunay(mapper.interpolator.mesh_grid_xy) simplex_index_for_sub_slim_index = delaunay.find_simplex( mapper.source_plane_data_grid ) - pix_indexes_for_simplex_index = mapper.delaunay.simplices + + delaunay = mapper.interpolator.delaunay + + pix_indexes_for_simplex_index = delaunay.simplices pix_indexes_for_sub_slim_index_util = pix_indexes_for_sub_slim_index_delaunay_from( - source_plane_data_grid=mapper.source_plane_data_grid.array, + data_grid=mapper.source_plane_data_grid.array, simplex_index_for_sub_slim_index=simplex_index_for_sub_slim_index, pix_indexes_for_simplex_index=pix_indexes_for_simplex_index, - delaunay_points=mapper.delaunay.points, + delaunay_points=delaunay.points, ) sizes = ( np.sum(pix_indexes_for_sub_slim_index_util >= 0, axis=1) @@ -75,81 +93,3 @@ def test__pix_indexes_for_sub_slim_index__matches_util(grid_2d_sub_1_7x7): assert ( mapper.pix_sizes_for_sub_slim_index == np.array([1, 1, 3, 1, 1, 1, 1, 1, 1]) ).all() - - -def test__scipy_delaunay__simplices(grid_2d_sub_1_7x7): - - 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, - over_sample_size=1, - ) - - mesh_grid = aa.Mesh2DDelaunay( - values=mesh_grid, source_plane_data_grid_over_sampled=grid_2d_sub_1_7x7 - ) - - assert (mesh_grid.delaunay.simplices[0, :] == np.array([3, 4, 0])).all() - assert (mesh_grid.delaunay.simplices[1, :] == np.array([3, 5, 4])).all() - assert (mesh_grid.delaunay.simplices[-1, :] == np.array([-1, -1, -1])).all() - - -def test__scipy_delaunay__split(grid_2d_sub_1_7x7): - - 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, - over_sample_size=1, - ) - - mesh_grid = aa.Mesh2DDelaunay( - values=mesh_grid, source_plane_data_grid_over_sampled=grid_2d_sub_1_7x7 - ) - - assert mesh_grid.delaunay.split_points[0, :] == pytest.approx( - [2.30929334, 0.1], 1.0e-4 - ) - assert mesh_grid.delaunay.split_points[1, :] == pytest.approx( - [-2.10929334, 0.1], 1.0e-4 - ) - assert mesh_grid.delaunay.split_points[-1, :] == pytest.approx( - [2.1, -1.10929334], 1.0e-4 - ) - - assert mesh_grid.delaunay.splitted_mappings[0, :] == pytest.approx( - [2, -1, -1], 1.0e-4 - ) - assert mesh_grid.delaunay.splitted_mappings[1, :] == pytest.approx( - [0, -1, -1], 1.0e-4 - ) - assert mesh_grid.delaunay.splitted_mappings[-1, :] == pytest.approx( - [2, -1, -1], 1.0e-4 - ) - - -def test__scipy_delaunay__split__uses_barycentric_dual_area_from(grid_2d_sub_1_7x7): - - 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, - over_sample_size=1, - ) - - mesh_grid = aa.Mesh2DDelaunay( - values=mesh_grid, - source_plane_data_grid_over_sampled=grid_2d_sub_1_7x7, - preloads=aa.Preloads(use_voronoi_areas=False), - ) - - assert mesh_grid.delaunay.split_points[0, :] == pytest.approx( - [0.45059473, 0.1], 1.0e-4 - ) - assert mesh_grid.delaunay.split_points[1, :] == pytest.approx( - [-0.25059473, 0.1], 1.0e-4 - ) - assert mesh_grid.delaunay.split_points[-1, :] == pytest.approx( - [2.1, 0.39142161], 1.0e-4 - ) diff --git a/test_autoarray/inversion/pixelization/mappers/test_factory.py b/test_autoarray/inversion/pixelization/mappers/test_factory.py index e6dbebdc9..b767359c5 100644 --- a/test_autoarray/inversion/pixelization/mappers/test_factory.py +++ b/test_autoarray/inversion/pixelization/mappers/test_factory.py @@ -26,22 +26,16 @@ def test__rectangular_mapper(): mesh = aa.mesh.RectangularUniform(shape=(3, 3)) - mapper_grids = mesh.mapper_grids_from( - mask=mask, - border_relocator=None, - source_plane_data_grid=grid, - source_plane_mesh_grid=None, + interpolator = mesh.interpolator_from( + source_plane_data_grid=grid, source_plane_mesh_grid=None ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) - - assert isinstance(mapper, aa.MapperRectangularUniform) - assert mapper.image_plane_mesh_grid == None + mapper = aa.Mapper(interpolator=interpolator) - assert mapper.source_plane_mesh_grid.geometry.shape_native_scaled == pytest.approx( + assert mapper.mesh_geometry.geometry.shape_native_scaled == pytest.approx( (5.0, 5.0), 1.0e-4 ) - assert mapper.source_plane_mesh_grid.origin == pytest.approx((0.5, 0.5), 1.0e-4) + assert mapper.mesh_geometry.origin == pytest.approx((0.5, 0.5), 1.0e-4) assert mapper.mapping_matrix == pytest.approx( np.array( [ @@ -54,7 +48,7 @@ def test__rectangular_mapper(): ), 1.0e-4, ) - assert mapper.shape_native == (3, 3) + assert mapper.mesh_geometry.shape_native == (3, 3) def test__delaunay_mapper(): @@ -81,18 +75,15 @@ def test__delaunay_mapper(): mask=mask, adapt_data=None ) - mapper_grids = mesh.mapper_grids_from( - mask=mask, - border_relocator=None, + interpolator = mesh.interpolator_from( source_plane_data_grid=grid, source_plane_mesh_grid=image_plane_mesh_grid, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) + mapper = aa.Mapper(interpolator=interpolator) - assert isinstance(mapper, aa.MapperDelaunay) assert (mapper.source_plane_mesh_grid == image_plane_mesh_grid).all() - assert mapper.source_plane_mesh_grid.origin == pytest.approx((0.0, 0.0), 1.0e-4) + assert mapper.mesh_geometry.origin == pytest.approx((0.0, 0.0), 1.0e-4) assert mapper.mapping_matrix == pytest.approx( np.array( diff --git a/test_autoarray/inversion/pixelization/mappers/test_mapper_util.py b/test_autoarray/inversion/pixelization/mappers/test_mapper_util.py index f677acb83..004b08885 100644 --- a/test_autoarray/inversion/pixelization/mappers/test_mapper_util.py +++ b/test_autoarray/inversion/pixelization/mappers/test_mapper_util.py @@ -2,9 +2,6 @@ import pytest import autoarray as aa -from autoarray.inversion.pixelization.mappers.delaunay import ( - pixel_weights_delaunay_from, -) @pytest.fixture(name="three_pixels") @@ -338,22 +335,6 @@ def test__data_to_pix_unique_from(): assert (pix_lengths == np.array([3, 3])).all() -def test__pixel_weights_delaunay_from(): - source_plane_data_grid = np.array([[0.1, 0.1], [1.0, 1.0]]) - - source_plane_mesh_grid = np.array([[0.0, 0.0], [0.1, 0.0], [0.2, 0.0]]) - - pix_indexes_for_sub_slim_index = np.array([[0, 1, 2], [2, -1, -1]]) - - pixel_weights = pixel_weights_delaunay_from( - source_plane_data_grid=source_plane_data_grid, - source_plane_mesh_grid=source_plane_mesh_grid, - pix_indexes_for_sub_slim_index=pix_indexes_for_sub_slim_index, - ) - - assert (pixel_weights == np.array([[0.25, 0.5, 0.25], [1.0, 0.0, 0.0]])).all() - - def test__adaptive_pixel_signals_from(): pix_indexes_for_sub_slim_index = np.array([[0], [1], [2]]) pixel_weights = np.ones((3, 1), dtype="int") diff --git a/test_autoarray/inversion/pixelization/mappers/test_rectangular.py b/test_autoarray/inversion/pixelization/mappers/test_rectangular.py index 866b25a7b..bfb7d2fde 100644 --- a/test_autoarray/inversion/pixelization/mappers/test_rectangular.py +++ b/test_autoarray/inversion/pixelization/mappers/test_rectangular.py @@ -1,8 +1,14 @@ -import numpy as np -import pytest - import autoarray as aa +from autoarray.inversion.mesh.mesh.rectangular_adapt_density import ( + overlay_grid_from, +) +from autoarray.inversion.mesh.interpolator.rectangular_uniform import ( + rectangular_mappings_weights_via_interpolation_from, +) + +import pytest + def test__pix_indexes_for_sub_slim_index__matches_util(): grid = aa.Grid2D.no_mask( @@ -22,43 +28,44 @@ def test__pix_indexes_for_sub_slim_index__matches_util(): over_sample_size=1, ) - mesh_grid = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(3, 3), grid=grid.over_sampled + mesh_grid = overlay_grid_from( + shape_native=(3, 3), grid=grid.over_sampled, buffer=1e-8 ) - mapper_grids = aa.MapperGrids( - mask=grid.mask, source_plane_data_grid=grid, source_plane_mesh_grid=mesh_grid + mesh = aa.mesh.RectangularUniform(shape=(3, 3)) + + interpolator = mesh.interpolator_from( + source_plane_data_grid=grid, + source_plane_mesh_grid=aa.Grid2DIrregular(mesh_grid), ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) + mapper = aa.Mapper(interpolator=interpolator) - mappings, weights = ( - aa.util.mapper.rectangular_mappings_weights_via_interpolation_from( - shape_native=(3, 3), - source_plane_mesh_grid=mesh_grid.array, - source_plane_data_grid=aa.Grid2DIrregular( - mapper_grids.source_plane_data_grid.over_sampled - ).array, - ) + mappings, weights = rectangular_mappings_weights_via_interpolation_from( + shape_native=(3, 3), + mesh_grid=aa.Grid2DIrregular(mesh_grid), + data_grid=aa.Grid2DIrregular(grid.over_sampled).array, ) - assert (mapper.pix_sub_weights.mappings == mappings).all() - assert (mapper.pix_sub_weights.weights == weights).all() + assert mapper.pix_indexes_for_sub_slim_index == pytest.approx(mappings, 1.0e-4) + assert mapper.pix_weights_for_sub_slim_index == pytest.approx(weights, 1.0e-4) def test__pixel_signals_from__matches_util(grid_2d_sub_1_7x7, image_7x7): - mesh_grid = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(3, 3), grid=grid_2d_sub_1_7x7.over_sampled + + mesh_grid = overlay_grid_from( + shape_native=(3, 3), grid=grid_2d_sub_1_7x7.over_sampled, buffer=1e-8 ) - mapper_grids = aa.MapperGrids( - mask=grid_2d_sub_1_7x7.mask, + mesh = aa.mesh.RectangularAdaptDensity(shape=(3, 3)) + + interpolator = mesh.interpolator_from( source_plane_data_grid=grid_2d_sub_1_7x7, source_plane_mesh_grid=mesh_grid, adapt_data=image_7x7, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) + mapper = aa.Mapper(interpolator=interpolator) pixel_signals = mapper.pixel_signals_from(signal_scale=2.0) @@ -73,73 +80,3 @@ def test__pixel_signals_from__matches_util(grid_2d_sub_1_7x7, image_7x7): ) assert (pixel_signals == pixel_signals_util).all() - - -def test__areas_transformed(mask_2d_7x7): - - grid = aa.Grid2DIrregular( - [ - [-1.5, -1.5], - [-1.5, 0.0], - [-1.5, 1.5], - [0.0, -1.5], - [0.0, 0.0], - [0.0, 1.5], - [1.5, -1.5], - [1.5, 0.0], - [1.5, 1.5], - ], - ) - - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(3, 3), grid=grid, buffer=1e-8 - ) - - mapper_grids = aa.MapperGrids( - mask=mask_2d_7x7, - source_plane_data_grid=grid, - source_plane_mesh_grid=mesh, - ) - - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) - - assert mapper.areas_transformed[4] == pytest.approx( - 4.0, - abs=1e-8, - ) - - -def test__edges_transformed(mask_2d_7x7): - - grid = aa.Grid2DIrregular( - [ - [-1.5, -1.5], - [-1.5, 0.0], - [-1.5, 1.5], - [0.0, -1.5], - [0.0, 0.0], - [0.0, 1.5], - [1.5, -1.5], - [1.5, 0.0], - [1.5, 1.5], - ], - ) - - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(3, 3), grid=grid, buffer=1e-8 - ) - - mapper_grids = aa.MapperGrids( - mask=mask_2d_7x7, - source_plane_data_grid=grid, - source_plane_mesh_grid=mesh, - ) - - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) - - assert mapper.edges_transformed[3] == pytest.approx( - np.array( - [-1.5, 1.5], # left - ), - abs=1e-8, - ) diff --git a/test_autoarray/inversion/pixelization/mesh/test_abstract.py b/test_autoarray/inversion/pixelization/mesh/test_abstract.py index c408cbff5..f070a9b1f 100644 --- a/test_autoarray/inversion/pixelization/mesh/test_abstract.py +++ b/test_autoarray/inversion/pixelization/mesh/test_abstract.py @@ -24,49 +24,38 @@ def test__grid_is_relocated_via_border(grid_2d_7x7): border_relocator = aa.BorderRelocator(mask=mask, sub_size=1) - mapper_grids = mesh.mapper_grids_from( - mask=mask, + interpolator = mesh.interpolator_from( border_relocator=border_relocator, source_plane_data_grid=grid, source_plane_mesh_grid=image_mesh, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) - - assert grid[8, 0] != mapper.source_plane_data_grid[8, 0] - assert mapper.source_plane_data_grid[8, 0] < 5.0 + assert grid[8, 0] != interpolator.data_grid[8, 0] + assert interpolator.data_grid[8, 0] < 5.0 grid[0, 0] = 0.0 image_mesh[0, 0] = 100.0 border_relocator = aa.BorderRelocator(mask=mask, sub_size=1) - mapper_grids = mesh.mapper_grids_from( - mask=mask, + interpolator = mesh.interpolator_from( border_relocator=border_relocator, source_plane_data_grid=grid, source_plane_mesh_grid=image_mesh, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) - - assert isinstance(mapper, aa.MapperDelaunay) - assert image_mesh[0, 0] != mapper.source_plane_mesh_grid[0, 0] - assert mapper.source_plane_mesh_grid[0, 0] < 5.0 + assert image_mesh[0, 0] != interpolator.mesh_grid[0, 0] + assert interpolator.mesh_grid[0, 0] < 5.0 mesh = aa.mesh.Delaunay() border_relocator = aa.BorderRelocator(mask=mask, sub_size=1) - mapper_grids = mesh.mapper_grids_from( - mask=mask, + interpolator = mesh.interpolator_from( border_relocator=border_relocator, source_plane_data_grid=grid, source_plane_mesh_grid=image_mesh, ) - mapper = aa.Mapper(mapper_grids=mapper_grids, regularization=None) - - assert isinstance(mapper, aa.MapperDelaunay) - assert image_mesh[0, 0] != mapper.source_plane_mesh_grid[0, 0] - assert mapper.source_plane_mesh_grid[0, 0] < 5.0 + assert image_mesh[0, 0] != interpolator.mesh_grid[0, 0] + assert interpolator.mesh_grid[0, 0] < 5.0 diff --git a/test_autoarray/structures/mesh/test_rectangular.py b/test_autoarray/inversion/pixelization/mesh/test_rectangular.py similarity index 63% rename from test_autoarray/structures/mesh/test_rectangular.py rename to test_autoarray/inversion/pixelization/mesh/test_rectangular.py index af5f6ae26..a6958086f 100644 --- a/test_autoarray/structures/mesh/test_rectangular.py +++ b/test_autoarray/inversion/pixelization/mesh/test_rectangular.py @@ -1,30 +1,14 @@ import numpy as np import pytest -import scipy.spatial -from autoarray import exc import autoarray as aa +from autoarray.inversion.mesh.mesh.rectangular_adapt_density import ( + overlay_grid_from, +) -def test__neighbors__compare_to_mesh_util(): - # I0 I 1I 2I 3I - # I4 I 5I 6I 7I - # I8 I 9I10I11I - # I12I13I14I15I - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(7, 5), grid=aa.Grid2DIrregular(np.zeros((2, 2))), buffer=1e-8 - ) - - (neighbors_util, neighbors_sizes_util) = aa.util.mesh.rectangular_neighbors_from( - shape_native=(7, 5) - ) - - assert (mesh.neighbors == neighbors_util).all() - assert (mesh.neighbors.sizes == neighbors_sizes_util).all() - - -def test__shape_native_and_pixel_scales(): +def test__overlay_grid_from__shape_native_and_pixel_scales(): grid = aa.Grid2DIrregular( [ [-1.0, -1.0], @@ -39,9 +23,11 @@ def test__shape_native_and_pixel_scales(): ] ) - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(3, 3), grid=grid, buffer=1e-8 - ) + mesh = aa.mesh.RectangularUniform(shape=(3, 3)) + + mesh_grid = overlay_grid_from(shape_native=mesh.shape, grid=grid, buffer=1e-8) + + mesh = aa.MeshGeometryRectangular(mesh=mesh, mesh_grid=mesh_grid, data_grid=None) assert mesh.shape_native == (3, 3) assert mesh.pixel_scales == pytest.approx((2.0 / 3.0, 2.0 / 3.0), 1e-2) @@ -60,24 +46,28 @@ def test__shape_native_and_pixel_scales(): ] ) - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(5, 4), grid=grid, buffer=1e-8 - ) + mesh = aa.mesh.RectangularUniform(shape=(5, 4)) + + mesh_grid = overlay_grid_from(shape_native=mesh.shape, grid=grid, buffer=1e-8) + + mesh = aa.MeshGeometryRectangular(mesh=mesh, mesh_grid=mesh_grid, data_grid=None) assert mesh.shape_native == (5, 4) assert mesh.pixel_scales == pytest.approx((2.0 / 5.0, 2.0 / 4.0), 1e-2) grid = aa.Grid2DIrregular([[2.0, 1.0], [4.0, 3.0], [6.0, 5.0], [8.0, 7.0]]) - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(3, 3), grid=grid, buffer=1e-8 - ) + mesh = aa.mesh.RectangularUniform(shape=(3, 3)) + + mesh_grid = overlay_grid_from(shape_native=mesh.shape, grid=grid, buffer=1e-8) + + mesh = aa.MeshGeometryRectangular(mesh=mesh, mesh_grid=mesh_grid, data_grid=None) assert mesh.shape_native == (3, 3) assert mesh.pixel_scales == pytest.approx((6.0 / 3.0, 6.0 / 3.0), 1e-2) -def test__pixel_centres__3x3_grid__pixel_centres(): +def test__overlay_grid_from__pixel_centres__3x3_grid__pixel_centres(): grid = aa.Grid2DIrregular( [ [1.0, -1.0], @@ -92,11 +82,9 @@ def test__pixel_centres__3x3_grid__pixel_centres(): ] ) - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(3, 3), grid=grid, buffer=1e-8 - ) + mesh_grid = overlay_grid_from(shape_native=(3, 3), grid=grid, buffer=1e-8) - assert mesh == pytest.approx( + assert mesh_grid == pytest.approx( np.array( [ [2.0 / 3.0, -2.0 / 3.0], @@ -126,11 +114,9 @@ def test__pixel_centres__3x3_grid__pixel_centres(): ] ) - mesh = aa.Mesh2DRectangularUniform.overlay_grid( - shape_native=(4, 3), grid=grid, buffer=1e-8 - ) + mesh_grid = overlay_grid_from(shape_native=(4, 3), grid=grid, buffer=1e-8) - assert mesh == pytest.approx( + assert mesh_grid == pytest.approx( np.array( [ [0.75, -2.0 / 3.0], diff --git a/test_autoarray/inversion/pixelization/mesh_geometry/__init__.py b/test_autoarray/inversion/pixelization/mesh_geometry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_autoarray/structures/mesh/test_delaunay.py b/test_autoarray/inversion/pixelization/mesh_geometry/test_delaunay.py similarity index 71% rename from test_autoarray/structures/mesh/test_delaunay.py rename to test_autoarray/inversion/pixelization/mesh_geometry/test_delaunay.py index 072fa5f91..3300bcb9d 100644 --- a/test_autoarray/structures/mesh/test_delaunay.py +++ b/test_autoarray/inversion/pixelization/mesh_geometry/test_delaunay.py @@ -13,11 +13,13 @@ def test__neighbors(grid_2d_sub_1_7x7): over_sample_size=1, ) - mesh_grid = aa.Mesh2DDelaunay( - values=mesh_grid, source_plane_data_grid_over_sampled=grid_2d_sub_1_7x7 + mesh_geometry = aa.MeshGeometryDelaunay( + mesh=aa.mesh.Delaunay(), + mesh_grid=mesh_grid, + data_grid=grid_2d_sub_1_7x7, ) - neighbors = mesh_grid.neighbors + neighbors = mesh_geometry.neighbors expected = np.array( [ @@ -38,13 +40,14 @@ def test__neighbors(grid_2d_sub_1_7x7): def test__voronoi_areas_via_delaunay_from(grid_2d_sub_1_7x7): - mesh_grid = np.array( + mesh_grid = aa.Grid2DIrregular( [[0.0, 0.0], [1.1, 0.6], [2.1, 0.1], [0.4, 1.1], [1.1, 7.1], [2.1, 1.1]] ) - mesh = aa.Mesh2DDelaunay( - values=mesh_grid, - source_plane_data_grid_over_sampled=grid_2d_sub_1_7x7.over_sampled, + mesh = aa.MeshGeometryDelaunay( + mesh=aa.mesh.Delaunay(), + mesh_grid=mesh_grid, + data_grid=grid_2d_sub_1_7x7.over_sampled, ) voronoi_areas = mesh.voronoi_areas diff --git a/test_autoarray/inversion/pixelization/mesh/test_mesh_util.py b/test_autoarray/inversion/pixelization/mesh_geometry/test_rectangular.py similarity index 51% rename from test_autoarray/inversion/pixelization/mesh/test_mesh_util.py rename to test_autoarray/inversion/pixelization/mesh_geometry/test_rectangular.py index 2a1c8ad78..eadeb26eb 100644 --- a/test_autoarray/inversion/pixelization/mesh/test_mesh_util.py +++ b/test_autoarray/inversion/pixelization/mesh_geometry/test_rectangular.py @@ -1,18 +1,24 @@ import numpy as np import pytest -import scipy.spatial import autoarray as aa +from autoarray.inversion.mesh.mesh.rectangular_adapt_density import ( + overlay_grid_from, +) + + +from autoarray.inversion.mesh.mesh_geometry.rectangular import ( + rectangular_neighbors_from, +) + def test__rectangular_neighbors_from(): # I0I1I2I # I3I4I5I # I6I7I8I - (neighbors, neighbors_sizes) = aa.util.mesh.rectangular_neighbors_from( - shape_native=(3, 3) - ) + (neighbors, neighbors_sizes) = rectangular_neighbors_from(shape_native=(3, 3)) # TODO : Use pytest.parameterize @@ -32,9 +38,7 @@ def test__rectangular_neighbors_from(): # I4I5I 6I 7I # I8I9I10I11I - (neighbors, neighbors_sizes) = aa.util.mesh.rectangular_neighbors_from( - shape_native=(3, 4) - ) + (neighbors, neighbors_sizes) = rectangular_neighbors_from(shape_native=(3, 4)) assert (neighbors[0] == [1, 4, -1, -1]).all() assert (neighbors[1] == [0, 2, 5, -1]).all() @@ -56,9 +60,7 @@ def test__rectangular_neighbors_from(): # I6I 7I 8I # I9I10I11I - (neighbors, neighbors_sizes) = aa.util.mesh.rectangular_neighbors_from( - shape_native=(4, 3) - ) + (neighbors, neighbors_sizes) = rectangular_neighbors_from(shape_native=(4, 3)) assert (neighbors[0] == [1, 3, -1, -1]).all() assert (neighbors[1] == [0, 2, 4, -1]).all() @@ -80,9 +82,7 @@ def test__rectangular_neighbors_from(): # I8 I 9I10I11I # I12I13I14I15I - (neighbors, neighbors_sizes) = aa.util.mesh.rectangular_neighbors_from( - shape_native=(4, 4) - ) + (neighbors, neighbors_sizes) = rectangular_neighbors_from(shape_native=(4, 4)) assert (neighbors[0] == [1, 4, -1, -1]).all() assert (neighbors[1] == [0, 2, 5, -1]).all() @@ -104,3 +104,99 @@ def test__rectangular_neighbors_from(): assert ( neighbors_sizes == np.array([2, 3, 3, 2, 3, 4, 4, 3, 3, 4, 4, 3, 2, 3, 3, 2]) ).all() + + +def test__neighbors__compare_to_mesh_util(): + # I0 I 1I 2I 3I + # I4 I 5I 6I 7I + # I8 I 9I10I11I + # I12I13I14I15I + + mesh = aa.mesh.RectangularUniform(shape=(7, 5)) + + mesh_grid = overlay_grid_from( + shape_native=mesh.shape, grid=aa.Grid2DIrregular(np.zeros((2, 2))), buffer=1e-8 + ) + + mesh_geometry = aa.MeshGeometryRectangular( + mesh=mesh, mesh_grid=mesh_grid, data_grid=None + ) + + (neighbors_util, neighbors_sizes_util) = rectangular_neighbors_from( + shape_native=(7, 5) + ) + + assert (mesh_geometry.neighbors == neighbors_util).all() + assert (mesh_geometry.neighbors.sizes == neighbors_sizes_util).all() + + +def test__areas_transformed(mask_2d_7x7): + + grid = aa.Grid2D.no_mask( + values=[ + [-1.5, -1.5], + [-1.5, 0.0], + [-1.5, 1.5], + [0.0, -1.5], + [0.0, 0.0], + [0.0, 1.5], + [1.5, -1.5], + [1.5, 0.0], + [1.5, 1.5], + ], + pixel_scales=1.5, + shape_native=(3, 3), + over_sample_size=1, + ) + + mesh_grid = overlay_grid_from(shape_native=(3, 3), grid=grid, buffer=1e-8) + + mesh = aa.mesh.RectangularAdaptDensity(shape=(3, 3)) + + interpolator = mesh.interpolator_from( + source_plane_data_grid=grid, + source_plane_mesh_grid=mesh_grid, + ) + + assert interpolator.mesh_geometry.areas_transformed[4] == pytest.approx( + 4.0, + abs=1e-8, + ) + + +def test__edges_transformed(mask_2d_7x7): + + grid = aa.Grid2D.no_mask( + values=[ + [-1.5, -1.5], + [-1.5, 0.0], + [-1.5, 1.5], + [0.0, -1.5], + [0.0, 0.0], + [0.0, 1.5], + [1.5, -1.5], + [1.5, 0.0], + [1.5, 1.5], + ], + pixel_scales=1.5, + shape_native=(3, 3), + over_sample_size=1, + ) + + mesh_grid = overlay_grid_from(shape_native=(3, 3), grid=grid, buffer=1e-8) + + mesh = aa.mesh.RectangularAdaptDensity(shape=(3, 3)) + + interpolator = mesh.interpolator_from( + source_plane_data_grid=grid, + source_plane_mesh_grid=mesh_grid, + ) + + mapper = aa.Mapper(interpolator=interpolator) + + assert interpolator.mesh_geometry.edges_transformed[3] == pytest.approx( + np.array( + [-1.5, 1.5], # left + ), + abs=1e-8, + ) diff --git a/test_autoarray/inversion/pixelization/test_border_relocator.py b/test_autoarray/inversion/pixelization/test_border_relocator.py index 2b6c3cd65..0430e25ed 100644 --- a/test_autoarray/inversion/pixelization/test_border_relocator.py +++ b/test_autoarray/inversion/pixelization/test_border_relocator.py @@ -3,7 +3,7 @@ import autoarray as aa -from autoarray.inversion.pixelization.border_relocator import ( +from autoarray.inversion.mesh.border_relocator import ( sub_border_pixel_slim_indexes_from, ) diff --git a/test_autoarray/inversion/regularizations/test_adapt.py b/test_autoarray/inversion/regularizations/test_adapt.py new file mode 100644 index 000000000..cfc4f78fc --- /dev/null +++ b/test_autoarray/inversion/regularizations/test_adapt.py @@ -0,0 +1,57 @@ +import autoarray as aa +import numpy as np +import pytest + + +def test__weight_list__matches_util(): + reg = aa.reg.Adapt(inner_coefficient=10.0, outer_coefficient=15.0) + + pixel_signals = np.array([0.21, 0.586, 0.45]) + + mapper = aa.m.MockMapper(pixel_signals=pixel_signals) + + weight_list = reg.regularization_weights_from(linear_obj=mapper) + + weight_list_util = aa.util.regularization.adapt_regularization_weights_from( + inner_coefficient=10.0, outer_coefficient=15.0, pixel_signals=pixel_signals + ) + + assert (weight_list == weight_list_util).all() + + +def test__regularization_matrix__matches_util(): + reg = aa.reg.Adapt(inner_coefficient=1.0, outer_coefficient=2.0, signal_scale=1.0) + + pixel_signals = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]) + + source_plane_mesh_grid = aa.Grid2D.no_mask( + values=[ + [0.1, 0.1], + [0.1, 0.2], + [0.1, 0.3], + [0.2, 0.1], + [0.2, 0.2], + [0.2, 0.3], + [0.3, 0.1], + [0.3, 0.2], + [0.3, 0.3], + ], + shape_native=(3, 3), + 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, + pixel_signals=pixel_signals, + mesh_geometry=mesh_geometry, + ) + + regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) + + assert regularization_matrix[0, 0] == pytest.approx(18.0000000, 1.0e-4) diff --git a/test_autoarray/inversion/regularizations/test_adaptive_brightness_split_zeroth.py b/test_autoarray/inversion/regularizations/test_adapt_split_zeroth.py similarity index 88% rename from test_autoarray/inversion/regularizations/test_adaptive_brightness_split_zeroth.py rename to test_autoarray/inversion/regularizations/test_adapt_split_zeroth.py index 23369606a..06feb9fdb 100644 --- a/test_autoarray/inversion/regularizations/test_adaptive_brightness_split_zeroth.py +++ b/test_autoarray/inversion/regularizations/test_adapt_split_zeroth.py @@ -5,7 +5,7 @@ def test__regularization_matrix_from(delaunay_mapper_9_3x3): - reg = aa.reg.AdaptiveBrightnessSplit( + reg = aa.reg.AdaptSplit( inner_coefficient=1.0, outer_coefficient=2.0, signal_scale=1.0 ) @@ -23,7 +23,7 @@ def test__regularization_matrix_from(delaunay_mapper_9_3x3): regularization_matrix_adaptive + regularization_matrix_zeroth ) - reg = aa.reg.AdaptiveBrightnessSplitZeroth( + reg = aa.reg.AdaptSplitZeroth( inner_coefficient=1.0, outer_coefficient=2.0, signal_scale=1.0, diff --git a/test_autoarray/inversion/regularizations/test_adaptive_brightness.py b/test_autoarray/inversion/regularizations/test_adaptive_brightness.py deleted file mode 100644 index b3cf4f132..000000000 --- a/test_autoarray/inversion/regularizations/test_adaptive_brightness.py +++ /dev/null @@ -1,61 +0,0 @@ -import autoarray as aa -import numpy as np - - -def test__weight_list__matches_util(): - reg = aa.reg.AdaptiveBrightness(inner_coefficient=10.0, outer_coefficient=15.0) - - pixel_signals = np.array([0.21, 0.586, 0.45]) - - mapper = aa.m.MockMapper(pixel_signals=pixel_signals) - - weight_list = reg.regularization_weights_from(linear_obj=mapper) - - weight_list_util = aa.util.regularization.adaptive_regularization_weights_from( - inner_coefficient=10.0, outer_coefficient=15.0, pixel_signals=pixel_signals - ) - - assert (weight_list == weight_list_util).all() - - -def test__regularization_matrix__matches_util(): - reg = aa.reg.AdaptiveBrightness( - inner_coefficient=1.0, outer_coefficient=2.0, signal_scale=1.0 - ) - - neighbors = np.array( - [ - [1, 4, -1, -1], - [2, 4, 0, -1], - [3, 4, 5, 1], - [5, 2, -1, -1], - [5, 0, 1, 2], - [2, 3, 4, -1], - ] - ) - - neighbors_sizes = np.array([2, 3, 4, 2, 4, 3]) - pixel_signals = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) - - mesh_grid = aa.m.MockMeshGrid(neighbors=neighbors, neighbors_sizes=neighbors_sizes) - - mapper = aa.m.MockMapper( - source_plane_mesh_grid=mesh_grid, pixel_signals=pixel_signals - ) - - regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) - - regularization_weights = ( - aa.util.regularization.adaptive_regularization_weights_from( - pixel_signals=pixel_signals, inner_coefficient=1.0, outer_coefficient=2.0 - ) - ) - - regularization_matrix_util = ( - aa.util.regularization.weighted_regularization_matrix_from( - regularization_weights=regularization_weights, - neighbors=neighbors, - ) - ) - - assert (regularization_matrix == regularization_matrix_util).all() diff --git a/test_autoarray/inversion/regularizations/test_constant.py b/test_autoarray/inversion/regularizations/test_constant.py index 44f45e096..2321b6c2f 100644 --- a/test_autoarray/inversion/regularizations/test_constant.py +++ b/test_autoarray/inversion/regularizations/test_constant.py @@ -1,42 +1,30 @@ import autoarray as aa import numpy as np +import pytest np.set_printoptions(threshold=np.inf) -def test__regularization_matrix__matches_util(): - neighbors = np.array( - [ - [1, 3, 7, 2], - [4, 2, 0, -1], - [1, 5, 3, -1], - [4, 6, 0, -1], - [7, 1, 5, 3], - [4, 2, 8, -1], - [7, 3, 0, -1], - [4, 8, 6, -1], - [7, 5, -1, -1], - ] - ) - - neighbors_sizes = np.array([4, 3, 3, 3, 4, 3, 3, 3, 2]) - - mesh_grid = aa.m.MockMeshGrid(neighbors=neighbors, neighbors_sizes=neighbors_sizes) - - mapper = aa.m.MockMapper(source_plane_mesh_grid=mesh_grid) +def test__regularization_matrix(): reg = aa.reg.Constant(coefficient=2.0) - regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) - regularization_matrix_util = ( - aa.util.regularization.constant_regularization_matrix_from( - coefficient=2.0, neighbors=neighbors, neighbors_sizes=neighbors_sizes - ) + 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, ) - assert reg.coefficient == 2.0 - assert (regularization_matrix == regularization_matrix_util).all() + 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 + ) - reg = aa.reg.ConstantSplit(coefficient=3.0) + regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) - assert reg.coefficient == 3.0 + assert regularization_matrix[0, 0] == pytest.approx(8.0000001, 1.0e-4) diff --git a/test_autoarray/inversion/regularizations/test_matern_adapt_kernel.py b/test_autoarray/inversion/regularizations/test_matern_adapt_kernel.py new file mode 100644 index 000000000..e5de0d28b --- /dev/null +++ b/test_autoarray/inversion/regularizations/test_matern_adapt_kernel.py @@ -0,0 +1,34 @@ +import pytest + +import autoarray as aa +import numpy as np + +np.set_printoptions(threshold=np.inf) + + +def test__regularization_matrix(): + + reg = aa.reg.MaternAdaptKernel( + scale=0.1, + nu=0.5, + inner_coefficient=0.1, + outer_coefficient=0.2, + signal_scale=0.1, + ) + + pixel_signals = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.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, + ) + + mapper = aa.m.MockMapper( + source_plane_mesh_grid=source_plane_mesh_grid, pixel_signals=pixel_signals + ) + + regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) + + # assert regularization_matrix[0, 0] == pytest.approx(18.7439565009, 1.0e-4) + # assert regularization_matrix[0, 1] == pytest.approx(-8.786547368, 1.0e-4) diff --git a/test_autoarray/inversion/regularizations/test_matern_adaptive_brightness_kernel.py b/test_autoarray/inversion/regularizations/test_matern_adaptive_brightness_kernel.py deleted file mode 100644 index de514975f..000000000 --- a/test_autoarray/inversion/regularizations/test_matern_adaptive_brightness_kernel.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest - -import autoarray as aa -import numpy as np - -np.set_printoptions(threshold=np.inf) - - -def test__regularization_matrix(): - - reg = aa.reg.MaternAdaptiveBrightnessKernel( - coefficient=1.0, scale=2.0, nu=2.0, rho=1.0 - ) - - pixel_signals = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.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, - ) - - mapper = aa.m.MockMapper( - source_plane_mesh_grid=source_plane_mesh_grid, pixel_signals=pixel_signals - ) - - regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) - - assert regularization_matrix[0, 0] == pytest.approx(18.7439565009, 1.0e-4) - assert regularization_matrix[0, 1] == pytest.approx(-8.786547368, 1.0e-4) - - reg = aa.reg.MaternAdaptiveBrightnessKernel( - coefficient=1.5, scale=2.5, nu=2.5, rho=1.5 - ) - - pixel_signals = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) - - mapper = aa.m.MockMapper( - source_plane_mesh_grid=source_plane_mesh_grid, pixel_signals=pixel_signals - ) - - regularization_matrix = reg.regularization_matrix_from(linear_obj=mapper) - - assert regularization_matrix[0, 0] == pytest.approx(121.0190770, 1.0e-4) - assert regularization_matrix[0, 1] == pytest.approx(-66.9580331, 1.0e-4) diff --git a/test_autoarray/inversion/regularizations/test_regularization_util.py b/test_autoarray/inversion/regularizations/test_regularization_util.py index 37a5a6687..45ba8bd6a 100644 --- a/test_autoarray/inversion/regularizations/test_regularization_util.py +++ b/test_autoarray/inversion/regularizations/test_regularization_util.py @@ -164,10 +164,10 @@ def test__constant_zeroth_regularization_matrix_from(): assert abs(np.linalg.det(regularization_matrix)) > 1e-8 -def test__adaptive_regularization_weights_from(): +def test__adapt_regularization_weights_from(): pixel_signals = np.array([1.0, 1.0, 1.0]) - weight_list = aa.util.regularization.adaptive_regularization_weights_from( + weight_list = aa.util.regularization.adapt_regularization_weights_from( inner_coefficient=1.0, outer_coefficient=1.0, pixel_signals=pixel_signals ) @@ -175,7 +175,7 @@ def test__adaptive_regularization_weights_from(): pixel_signals = np.array([0.25, 0.5, 0.75]) - weight_list = aa.util.regularization.adaptive_regularization_weights_from( + weight_list = aa.util.regularization.adapt_regularization_weights_from( inner_coefficient=1.0, outer_coefficient=1.0, pixel_signals=pixel_signals ) @@ -183,7 +183,7 @@ def test__adaptive_regularization_weights_from(): pixel_signals = np.array([0.25, 0.5, 0.75]) - weight_list = aa.util.regularization.adaptive_regularization_weights_from( + weight_list = aa.util.regularization.adapt_regularization_weights_from( inner_coefficient=1.0, outer_coefficient=0.0, pixel_signals=pixel_signals ) @@ -191,7 +191,7 @@ def test__adaptive_regularization_weights_from(): pixel_signals = np.array([0.25, 0.5, 0.75]) - weight_list = aa.util.regularization.adaptive_regularization_weights_from( + weight_list = aa.util.regularization.adapt_regularization_weights_from( inner_coefficient=0.0, outer_coefficient=1.0, pixel_signals=pixel_signals ) diff --git a/test_autoarray/structures/mesh/test_abstract.py b/test_autoarray/structures/mesh/test_abstract.py deleted file mode 100644 index 78506ab86..000000000 --- a/test_autoarray/structures/mesh/test_abstract.py +++ /dev/null @@ -1,48 +0,0 @@ -import numpy as np -import pytest - -import autoarray as aa - - -def test__interpolation_grid_from(): - mesh = aa.m.MockGrid2DMesh(extent=(-1.0, 1.0, -1.0, 1.0)) - - interpolation_grid = mesh.interpolation_grid_from(shape_native=(3, 2)) - - assert interpolation_grid.native == pytest.approx( - np.array( - [ - [[1.0, -1.0], [1.0, 1.0]], - [[0.0, -1.0], [0.0, 1.0]], - [[-1.0, -1.0], [-1.0, 1.0]], - ] - ) - ) - assert interpolation_grid.pixel_scales == (1.0, 2.0) - - interpolation_grid = mesh.interpolation_grid_from(shape_native=(2, 3)) - - assert interpolation_grid.native == pytest.approx( - np.array( - [ - [[1.0, -1.0], [1.0, 0.0], [1.0, 1.0]], - [[-1.0, -1.0], [-1.0, 0.0], [-1.0, 1.0]], - ] - ) - ) - assert interpolation_grid.pixel_scales == (2.0, 1.0) - - mesh = aa.m.MockGrid2DMesh(extent=(-20.0, -5.0, -10.0, -5.0)) - - interpolation_grid = mesh.interpolation_grid_from(shape_native=(3, 3)) - - assert interpolation_grid.native == pytest.approx( - np.array( - [ - [[0.0, -20.0], [0.0, -12.5], [0.0, -5.0]], - [[-7.5, -20.0], [-7.5, -12.5], [-7.5, -5.0]], - [[-15.0, -20.0], [-15.0, -12.5], [-15.0, -5.0]], - ] - ) - ) - assert interpolation_grid.pixel_scales == (7.5, 7.5)