From 46b1f3a95a552d69ad02d11285576be84cb59b9e Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Thu, 28 Dec 2023 17:32:45 +0000 Subject: [PATCH 1/4] implementation working and tested --- .../pixelization/image_mesh/__init__.py | 1 + .../image_mesh/hilbert_background.py | 132 ++++++++++++++++++ .../image_mesh/test_hilbert_background.py | 29 ++++ 3 files changed, 162 insertions(+) create mode 100644 autoarray/inversion/pixelization/image_mesh/hilbert_background.py create mode 100644 test_autoarray/inversion/pixelization/image_mesh/test_hilbert_background.py diff --git a/autoarray/inversion/pixelization/image_mesh/__init__.py b/autoarray/inversion/pixelization/image_mesh/__init__.py index faf08b64..b6ed4349 100644 --- a/autoarray/inversion/pixelization/image_mesh/__init__.py +++ b/autoarray/inversion/pixelization/image_mesh/__init__.py @@ -1,3 +1,4 @@ from .hilbert import Hilbert +from .hilbert_background import HilbertBackground from .overlay import Overlay from .kmeans import KMeans diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert_background.py b/autoarray/inversion/pixelization/image_mesh/hilbert_background.py new file mode 100644 index 00000000..2d878ea7 --- /dev/null +++ b/autoarray/inversion/pixelization/image_mesh/hilbert_background.py @@ -0,0 +1,132 @@ +from __future__ import annotations +import numpy as np +from scipy.interpolate import interp1d, griddata +from typing import Optional + +from autoarray.structures.grids.uniform_2d import Grid2D +from autoarray.mask.mask_2d import Mask2D +from autoarray.inversion.pixelization.image_mesh.abstract_weighted import ( + AbstractImageMeshWeighted, +) +from autoarray.structures.grids.irregular_2d import Grid2DIrregular + +from autoarray.inversion.pixelization.image_mesh.hilbert import image_and_grid_from +from autoarray.inversion.pixelization.image_mesh.hilbert import inverse_transform_sampling_interpolated + +from autoarray import exc + +class HilbertBackground(AbstractImageMeshWeighted): + def __init__( + self, + pixels=10.0, + weight_floor=0.0, + weight_power=0.0, + ): + """ + Computes an image-mesh by computing the Hilbert curve of the adapt data and drawing points from it. + + This requires an adapt-image, which is the image that the Hilbert curve algorithm adapts to in order to compute + the image mesh. This could simply be the image itself, or a model fit to the image which removes certain + features or noise. + + For example, using the adapt image, the image mesh is computed as follows: + + 1) Convert the adapt image to a weight map, which is a 2D array of weight values. + + 2) Run the Hilbert algorithm on the weight map, such that the image mesh pixels cluster around the weight map + values with higher values. + + Parameters + ---------- + pixels + The total number of pixels in the image mesh and drawn from the Hilbert curve. + weight_floor + The minimum weight value in the weight map, which allows more pixels to be drawn from the lower weight + regions of the adapt image. + weight_power + The power the weight values are raised too, which allows more pixels to be drawn from the higher weight + regions of the adapt image. + """ + + super().__init__( + pixels=pixels, + weight_floor=weight_floor, + weight_power=weight_power, + ) + + def image_plane_mesh_grid_from( + self, grid: Grid2D, adapt_data: Optional[np.ndarray] + ) -> Grid2DIrregular: + """ + Returns an image mesh by running the Hilbert curve on the weight map. + + See the `__init__` docstring for a full description of how this is performed. + + Parameters + ---------- + grid + The grid of (y,x) coordinates of the image data the pixelization fits, which the Hilbert curve adapts to. + adapt_data + The weights defining the regions of the image the Hilbert curve adapts to. + + Returns + ------- + + """ + + if not grid.mask.is_circular: + raise exc.PixelizationException( + """ + Hilbert image-mesh has been called but the input grid does not use a circular mask. + + Ensure that analysis is using a circular mask via the Mask2D.circular classmethod. + """ + ) + + adapt_data_hb, grid_hb = image_and_grid_from( + image=adapt_data, + mask=grid.mask, + mask_radius=grid.mask.circular_radius, + pixel_scales=grid.mask.pixel_scales, + hilbert_length=193, + ) + + weight_map = self.weight_map_from(adapt_data=adapt_data_hb) + + weight_map_background = 1.0 - weight_map + + weight_map /= np.sum(weight_map) + weight_map_background /= np.sum(weight_map_background) + + if self.pixels % 2 == 1: + pixels = self.pixels + 1 + else: + pixels = self.pixels + + ( + drawn_id, + drawn_x, + drawn_y, + ) = inverse_transform_sampling_interpolated( + probabilities=weight_map, + n_samples=pixels // 2, + gridx=grid_hb[:, 1], + gridy=grid_hb[:, 0], + ) + + grid = np.stack((drawn_y, drawn_x), axis=-1) + + ( + drawn_id, + drawn_x, + drawn_y, + ) = inverse_transform_sampling_interpolated( + probabilities=weight_map_background, + n_samples=(self.pixels // 2) + 1, + gridx=grid_hb[:, 1], + gridy=grid_hb[:, 0], + ) + + grid_background = np.stack((drawn_y, drawn_x), axis=-1) + + return Grid2DIrregular(values=np.concatenate((grid, grid_background[1:, :]), axis=0)) \ No newline at end of file diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_background.py b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_background.py new file mode 100644 index 00000000..0606b869 --- /dev/null +++ b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_background.py @@ -0,0 +1,29 @@ +import pytest + +import autoarray as aa + + +def test__image_plane_mesh_grid_from(): + mask = aa.Mask2D.circular( + shape_native=(4, 4), + radius=2.0, + pixel_scales=1.0, + sub_size=1, + ) + + grid = aa.Grid2D.from_mask(mask=mask) + + adapt_data = aa.Array2D.ones( + shape_native=mask.shape_native, + pixel_scales=1.0, + ) + + kmeans = aa.image_mesh.HilbertBackground(pixels=10, pixels_background=5) + image_mesh = kmeans.image_plane_mesh_grid_from(grid=grid, adapt_data=adapt_data) + + print(image_mesh) + + assert image_mesh[0, :] == pytest.approx( + [-1.02590674, -1.70984456], + 1.0e-4, + ) From 2f7aafe44de3dee01fcc2a6d47146dbf574b0c77 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Thu, 28 Dec 2023 17:33:37 +0000 Subject: [PATCH 2/4] rename to HilbertBalanced --- .../pixelization/image_mesh/__init__.py | 2 +- .../image_mesh/abstract_weighted.py | 2 +- ...bert_background.py => hilbert_balanced.py} | 21 ++++++++++++------- ...background.py => test_hilbert_balanced.py} | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) rename autoarray/inversion/pixelization/image_mesh/{hilbert_background.py => hilbert_balanced.py} (89%) rename test_autoarray/inversion/pixelization/image_mesh/{test_hilbert_background.py => test_hilbert_balanced.py} (84%) diff --git a/autoarray/inversion/pixelization/image_mesh/__init__.py b/autoarray/inversion/pixelization/image_mesh/__init__.py index b6ed4349..ae4f3185 100644 --- a/autoarray/inversion/pixelization/image_mesh/__init__.py +++ b/autoarray/inversion/pixelization/image_mesh/__init__.py @@ -1,4 +1,4 @@ from .hilbert import Hilbert -from .hilbert_background import HilbertBackground +from .hilbert_background import HilbertBalanced from .overlay import Overlay from .kmeans import KMeans diff --git a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py b/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py index e874fa2a..7f6d76d6 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py +++ b/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py @@ -65,7 +65,7 @@ def weight_map_from(self, adapt_data: np.ndarray): weight_map += self.weight_floor weight_map[weight_map > 1.0] = 1.0 - weight_map = weight_map ** self.weight_power + weight_map = weight_map**self.weight_power return weight_map diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert_background.py b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py similarity index 89% rename from autoarray/inversion/pixelization/image_mesh/hilbert_background.py rename to autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py index 2d878ea7..beeca9c0 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert_background.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py @@ -11,16 +11,19 @@ from autoarray.structures.grids.irregular_2d import Grid2DIrregular from autoarray.inversion.pixelization.image_mesh.hilbert import image_and_grid_from -from autoarray.inversion.pixelization.image_mesh.hilbert import inverse_transform_sampling_interpolated +from autoarray.inversion.pixelization.image_mesh.hilbert import ( + inverse_transform_sampling_interpolated, +) from autoarray import exc -class HilbertBackground(AbstractImageMeshWeighted): + +class HilbertBalanced(AbstractImageMeshWeighted): def __init__( - self, - pixels=10.0, - weight_floor=0.0, - weight_power=0.0, + self, + pixels=10.0, + weight_floor=0.0, + weight_power=0.0, ): """ Computes an image-mesh by computing the Hilbert curve of the adapt data and drawing points from it. @@ -55,7 +58,7 @@ def __init__( ) def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] + self, grid: Grid2D, adapt_data: Optional[np.ndarray] ) -> Grid2DIrregular: """ Returns an image mesh by running the Hilbert curve on the weight map. @@ -129,4 +132,6 @@ def image_plane_mesh_grid_from( grid_background = np.stack((drawn_y, drawn_x), axis=-1) - return Grid2DIrregular(values=np.concatenate((grid, grid_background[1:, :]), axis=0)) \ No newline at end of file + return Grid2DIrregular( + values=np.concatenate((grid, grid_background[1:, :]), axis=0) + ) diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_background.py b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_balanced.py similarity index 84% rename from test_autoarray/inversion/pixelization/image_mesh/test_hilbert_background.py rename to test_autoarray/inversion/pixelization/image_mesh/test_hilbert_balanced.py index 0606b869..60f13208 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_background.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_balanced.py @@ -18,7 +18,7 @@ def test__image_plane_mesh_grid_from(): pixel_scales=1.0, ) - kmeans = aa.image_mesh.HilbertBackground(pixels=10, pixels_background=5) + kmeans = aa.image_mesh.HilbertBalanced(pixels=10, pixels_background=5) image_mesh = kmeans.image_plane_mesh_grid_from(grid=grid, adapt_data=adapt_data) print(image_mesh) From 7f08379761591daca72c6eb37e62be0b140ac31b Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Thu, 28 Dec 2023 17:35:56 +0000 Subject: [PATCH 3/4] fix tests --- autoarray/inversion/pixelization/image_mesh/__init__.py | 2 +- .../inversion/pixelization/image_mesh/test_hilbert_balanced.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autoarray/inversion/pixelization/image_mesh/__init__.py b/autoarray/inversion/pixelization/image_mesh/__init__.py index ae4f3185..c5626dd8 100644 --- a/autoarray/inversion/pixelization/image_mesh/__init__.py +++ b/autoarray/inversion/pixelization/image_mesh/__init__.py @@ -1,4 +1,4 @@ from .hilbert import Hilbert -from .hilbert_background import HilbertBalanced +from .hilbert_balanced import HilbertBalanced from .overlay import Overlay from .kmeans import KMeans diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_balanced.py b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_balanced.py index 60f13208..6cedb2d0 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_balanced.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert_balanced.py @@ -18,7 +18,7 @@ def test__image_plane_mesh_grid_from(): pixel_scales=1.0, ) - kmeans = aa.image_mesh.HilbertBalanced(pixels=10, pixels_background=5) + kmeans = aa.image_mesh.HilbertBalanced(pixels=10) image_mesh = kmeans.image_plane_mesh_grid_from(grid=grid, adapt_data=adapt_data) print(image_mesh) From 218473495e7f6a68ec44e1750392514d4e1befc9 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Thu, 28 Dec 2023 17:38:11 +0000 Subject: [PATCH 4/4] implementation complete, docs --- .../pixelization/image_mesh/hilbert_balanced.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py index beeca9c0..b865e634 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py @@ -26,7 +26,15 @@ def __init__( weight_power=0.0, ): """ - Computes an image-mesh by computing the Hilbert curve of the adapt data and drawing points from it. + Computes a balanced image-mesh by computing the Hilbert curve of the adapt data and drawing points from it. + + The standard `Hilbert` image-mesh suffers a systematic where the vast majority of points are drawn from + the high weighted reigons. This often leaves few points to reconstruct the lower weight regions, leading to + discontinuities in the reconstruction. + + This image-mesh addresses this by drawing half the points from the weight map and the other half from + (1 - weight map). This ensures both high and low weighted regions are sampled equally, but still has sufficient + flexibility to dedicate many points to the highest weighted regions. This requires an adapt-image, which is the image that the Hilbert curve algorithm adapts to in order to compute the image mesh. This could simply be the image itself, or a model fit to the image which removes certain @@ -61,7 +69,7 @@ def image_plane_mesh_grid_from( self, grid: Grid2D, adapt_data: Optional[np.ndarray] ) -> Grid2DIrregular: """ - Returns an image mesh by running the Hilbert curve on the weight map. + Returns an image mesh by running the balanced Hilbert curve on the weight map. See the `__init__` docstring for a full description of how this is performed.