From 142e9a4627e07fa1f5a5262892ad39d86565a9f0 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 14 Jan 2024 11:50:01 +0000 Subject: [PATCH 1/5] Added method AbstractImageMesh.mesh_pixels_per_image_pixels_from --- .../pixelization/image_mesh/abstract.py | 74 +++++++++++++++++++ .../pixelization/image_mesh/hilbert.py | 4 +- autoarray/plot/mat_plot/one_d.py | 3 +- autoarray/plot/wrap/base/ticks.py | 4 + autoarray/plot/wrap/one_d/yx_plot.py | 1 + .../structures/plot/structure_plotters.py | 3 + .../pixelization/image_mesh/test_abstract.py | 32 ++++++++ 7 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 test_autoarray/inversion/pixelization/image_mesh/test_abstract.py diff --git a/autoarray/inversion/pixelization/image_mesh/abstract.py b/autoarray/inversion/pixelization/image_mesh/abstract.py index 4023f37d5..d57355176 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract.py +++ b/autoarray/inversion/pixelization/image_mesh/abstract.py @@ -2,9 +2,46 @@ import numpy as np +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 import numba_util +from autoarray.geometry import geometry_util + +@numba_util.jit() +def mesh_pixels_per_image_pixels_from( + grid_pixel_centres, + shape_native +) -> np.ndarray: + """ + Returns an array containing the number of mesh pixels in every pixel of the data's mask. + + For example, image-mesh adaption may be performed on a 3.0" circular mask of data. The high weight pixels + may have 3 or more mesh pixels per image pixel, whereas low weight regions may have zero pixels. The array + returned by this function gives the integer number of pixels in each data pixel. + + Parameters + ---------- + grid_pixel_centres + The 2D integer index of every image pixel that each image-mesh pixel falls within. + shape_native + The 2D shape of the data's mask, which the number of image-mesh pixels that fall within eac pixel is counted. + + Returns + ------- + An array containing the integer number of image-mesh pixels that fall without each of the data's mask. + """ + mesh_pixels_per_image_pixel = np.zeros(shape=shape_native) + + for i in range(grid_pixel_centres.shape[0]): + y = grid_pixel_centres[i, 0] + x = grid_pixel_centres[i, 1] + + mesh_pixels_per_image_pixel[y, x] += 1 + + return mesh_pixels_per_image_pixel + class AbstractImageMesh: def __init__(self): @@ -48,3 +85,40 @@ def image_plane_mesh_grid_from( self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None ) -> Grid2DIrregular: raise NotImplementedError + + def mesh_pixels_per_image_pixels_from(self, grid: Grid2D, mesh_grid : Grid2DIrregular) -> Array2D: + """ + Returns an array containing the number of mesh pixels in every pixel of the data's mask. + + For example, image-mesh adaption may be performed on a 3.0" circular mask of data. The high weight pixels + may have 3 or more mesh pixels per image pixel, whereas low weight regions may have zero pixels. The array + returned by this function gives the integer number of pixels in each data pixel. + + Parameters + ---------- + grid + The masked (y,x) grid of the data coordinates, corresponding to the mask applied to the data. The number of + mesh pixels mapped inside each of this grid's image-pixels is returned. + mesh_grid + The image mesh-grid computed by the class which adapts to the data's mask. The number of image mesh pixels + that fall within each of the data's mask pixels is returned. + + Returns + ------- + An array containing the integer number of image-mesh pixels that fall without each of the data's mask. + """ + + grid_pixel_centres = geometry_util.grid_pixel_centres_2d_slim_from( + grid_scaled_2d_slim=mesh_grid, + shape_native=grid.shape_native, + pixel_scales=grid.pixel_scales, + origin=grid.origin, + ).astype("int") + + mesh_pixels_per_image_pixels = mesh_pixels_per_image_pixels_from( + grid=grid, + grid_pixel_centres=grid_pixel_centres, + shape_native=grid.shape_native + ) + + return Array2D(values=mesh_pixels_per_image_pixels, mask=grid.mask) diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert.py b/autoarray/inversion/pixelization/image_mesh/hilbert.py index 776bc05de..eda281f5e 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert.py @@ -296,4 +296,6 @@ def image_plane_mesh_grid_from( gridy=grid_hb[:, 0], ) - return Grid2DIrregular(values=np.stack((drawn_y, drawn_x), axis=-1)) + mesh_grid = Grid2DIrregular(values=np.stack((drawn_y, drawn_x), axis=-1)) + + return mesh_grid \ No newline at end of file diff --git a/autoarray/plot/mat_plot/one_d.py b/autoarray/plot/mat_plot/one_d.py index bda51202b..306372292 100644 --- a/autoarray/plot/mat_plot/one_d.py +++ b/autoarray/plot/mat_plot/one_d.py @@ -251,6 +251,7 @@ def plot_yx( units=self.units, use_integers=use_integers, is_for_1d_plot=True, + is_log10="loglog" in plot_axis_type, ) self.yticks.set( @@ -260,7 +261,7 @@ def plot_yx( units=self.units, yunit=auto_labels.yunit, is_for_1d_plot=True, - is_log10="logy" in plot_axis_type, + is_log10="log" in plot_axis_type, ) self.title.set(auto_title=auto_labels.title) diff --git a/autoarray/plot/wrap/base/ticks.py b/autoarray/plot/wrap/base/ticks.py index 869fd343c..a6c9f89c4 100644 --- a/autoarray/plot/wrap/base/ticks.py +++ b/autoarray/plot/wrap/base/ticks.py @@ -133,6 +133,8 @@ def tick_values_rounded(self): @property def labels_linear(self): + + if self.units.use_raw: return self.with_appended_suffix(self.tick_values_rounded) @@ -410,6 +412,7 @@ def set( xunit=None, use_integers=False, is_for_1d_plot: bool = False, + is_log10: bool = False, ): """ Set the x ticks of a figure using the shape of an input `Array2D` object and input units. @@ -438,6 +441,7 @@ def set( yunit=xunit, use_integers=use_integers, is_for_1d_plot=is_for_1d_plot, + is_log10=is_log10 ) plt.xticks(ticks=ticks, labels=labels, **self.config_dict) diff --git a/autoarray/plot/wrap/one_d/yx_plot.py b/autoarray/plot/wrap/one_d/yx_plot.py index e8671f5b4..393af5ef6 100644 --- a/autoarray/plot/wrap/one_d/yx_plot.py +++ b/autoarray/plot/wrap/one_d/yx_plot.py @@ -81,5 +81,6 @@ def plot_y_vs_x( "{semilogy, loglog})" ) + if y_extra is not None: plt.plot(x, y_extra, c="r") diff --git a/autoarray/structures/plot/structure_plotters.py b/autoarray/structures/plot/structure_plotters.py index 97f099f85..17fcd8372 100644 --- a/autoarray/structures/plot/structure_plotters.py +++ b/autoarray/structures/plot/structure_plotters.py @@ -134,6 +134,7 @@ def __init__( should_plot_grid: bool = False, should_plot_zero: bool = False, plot_axis_type: Optional[str] = None, + plot_yx_dict=None ): """ Plots two 1D objects using the matplotlib method `plot()` (or a similar method) and many other matplotlib @@ -176,6 +177,7 @@ def __init__( self.should_plot_grid = should_plot_grid self.should_plot_zero = should_plot_zero self.plot_axis_type = plot_axis_type + self.plot_yx_dict = plot_yx_dict or {} def get_visuals_1d(self) -> Visuals1D: return self.get_1d.via_array_1d_from(array_1d=self.x) @@ -193,4 +195,5 @@ def figure_1d(self): should_plot_grid=self.should_plot_grid, should_plot_zero=self.should_plot_zero, plot_axis_type_override=self.plot_axis_type, + **self.plot_yx_dict ) diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py new file mode 100644 index 000000000..54ef7267a --- /dev/null +++ b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py @@ -0,0 +1,32 @@ +import numpy as np +import pytest + +import autoarray as aa + +def test__mesh_pixels_per_image_pixels_from(): + + mask = aa.Mask2D.circular( + shape_native=(3, 3), + radius=2.0, + pixel_scales=1.0, + sub_size=1, + ) + + grid = aa.Grid2D.from_mask(mask=mask) + + mesh_grid = aa.Grid2DIrregular( + values=[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (-1.0, -1.0)] + ) + + image_mesh = aa.image_mesh.Hilbert(pixels=8) + + mesh_pixels_per_image_pixels = image_mesh.mesh_pixels_per_image_pixels_from( + grid=grid, + mesh_grid=mesh_grid + ) + + assert mesh_pixels_per_image_pixels.native == pytest.approx(np.array( + [[0, 0, 0], + [0, 3, 0], + [1, 0 ,0]] + ), 1.0e-4) \ No newline at end of file From 994342b65bd9d1359f99db75542c208594a66300 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 14 Jan 2024 12:31:59 +0000 Subject: [PATCH 2/5] inversion plotter can now plot esh_pixels_per_image_pixels --- .../pixelization/image_mesh/abstract.py | 50 +++---------------- .../pixelization/image_mesh/hilbert.py | 2 +- .../image_mesh/hilbert_balanced.py | 2 +- .../pixelization/image_mesh/kmeans.py | 4 +- .../pixelization/mappers/mapper_grids.py | 20 ++++++++ .../inversion/plot/inversion_plotters.py | 18 +++++++ autoarray/plot/mat_plot/two_d.py | 8 ++- autoarray/plot/wrap/base/cmap.py | 2 +- autoarray/plot/wrap/base/colorbar.py | 21 ++++---- autoarray/plot/wrap/base/ticks.py | 4 +- autoarray/plot/wrap/base/title.py | 3 +- autoarray/plot/wrap/one_d/yx_plot.py | 1 - autoarray/plot/wrap/two_d/contour.py | 7 ++- autoarray/structures/grids/grid_2d_util.py | 40 +++++++++++++++ .../structures/plot/structure_plotters.py | 2 +- .../pixelization/image_mesh/test_abstract.py | 13 ++--- .../inversion/plot/test_inversion_plotters.py | 2 + 17 files changed, 123 insertions(+), 76 deletions(-) diff --git a/autoarray/inversion/pixelization/image_mesh/abstract.py b/autoarray/inversion/pixelization/image_mesh/abstract.py index d57355176..edb8d8f0d 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract.py +++ b/autoarray/inversion/pixelization/image_mesh/abstract.py @@ -6,41 +6,7 @@ from autoarray.structures.grids.uniform_2d import Grid2D from autoarray.structures.grids.irregular_2d import Grid2DIrregular -from autoarray import numba_util -from autoarray.geometry import geometry_util - -@numba_util.jit() -def mesh_pixels_per_image_pixels_from( - grid_pixel_centres, - shape_native -) -> np.ndarray: - """ - Returns an array containing the number of mesh pixels in every pixel of the data's mask. - - For example, image-mesh adaption may be performed on a 3.0" circular mask of data. The high weight pixels - may have 3 or more mesh pixels per image pixel, whereas low weight regions may have zero pixels. The array - returned by this function gives the integer number of pixels in each data pixel. - - Parameters - ---------- - grid_pixel_centres - The 2D integer index of every image pixel that each image-mesh pixel falls within. - shape_native - The 2D shape of the data's mask, which the number of image-mesh pixels that fall within eac pixel is counted. - - Returns - ------- - An array containing the integer number of image-mesh pixels that fall without each of the data's mask. - """ - mesh_pixels_per_image_pixel = np.zeros(shape=shape_native) - - for i in range(grid_pixel_centres.shape[0]): - y = grid_pixel_centres[i, 0] - x = grid_pixel_centres[i, 1] - - mesh_pixels_per_image_pixel[y, x] += 1 - - return mesh_pixels_per_image_pixel +from autoarray.structures.grids import grid_2d_util class AbstractImageMesh: @@ -86,7 +52,9 @@ def image_plane_mesh_grid_from( ) -> Grid2DIrregular: raise NotImplementedError - def mesh_pixels_per_image_pixels_from(self, grid: Grid2D, mesh_grid : Grid2DIrregular) -> Array2D: + def mesh_pixels_per_image_pixels_from( + self, grid: Grid2D, mesh_grid: Grid2DIrregular + ) -> Array2D: """ Returns an array containing the number of mesh pixels in every pixel of the data's mask. @@ -108,17 +76,11 @@ def mesh_pixels_per_image_pixels_from(self, grid: Grid2D, mesh_grid : Grid2DIrre An array containing the integer number of image-mesh pixels that fall without each of the data's mask. """ - grid_pixel_centres = geometry_util.grid_pixel_centres_2d_slim_from( - grid_scaled_2d_slim=mesh_grid, + mesh_pixels_per_image_pixels = grid_2d_util.grid_pixels_in_mask_pixels_from( + grid=mesh_grid, shape_native=grid.shape_native, pixel_scales=grid.pixel_scales, origin=grid.origin, - ).astype("int") - - mesh_pixels_per_image_pixels = mesh_pixels_per_image_pixels_from( - grid=grid, - grid_pixel_centres=grid_pixel_centres, - shape_native=grid.shape_native ) return Array2D(values=mesh_pixels_per_image_pixels, mask=grid.mask) diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert.py b/autoarray/inversion/pixelization/image_mesh/hilbert.py index eda281f5e..a9635655e 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert.py @@ -298,4 +298,4 @@ def image_plane_mesh_grid_from( mesh_grid = Grid2DIrregular(values=np.stack((drawn_y, drawn_x), axis=-1)) - return mesh_grid \ No newline at end of file + return mesh_grid diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py index 4cf6a183f..b895bbe11 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py @@ -135,7 +135,7 @@ def image_plane_mesh_grid_from( drawn_y, ) = inverse_transform_sampling_interpolated( probabilities=weight_map_background, - n_samples=(self.pixels - pixels) + 1, + n_samples=(self.pixels - pixels) + 1, gridx=grid_hb[:, 1], gridy=grid_hb[:, 0], ) diff --git a/autoarray/inversion/pixelization/image_mesh/kmeans.py b/autoarray/inversion/pixelization/image_mesh/kmeans.py index 158a9790f..c9c213de3 100644 --- a/autoarray/inversion/pixelization/image_mesh/kmeans.py +++ b/autoarray/inversion/pixelization/image_mesh/kmeans.py @@ -72,7 +72,6 @@ def image_plane_mesh_grid_from( """ if self.pixels > grid.shape[0]: - print( """ The number of pixels passed to the KMeans object exceeds the number of image-pixels in the mask of @@ -87,7 +86,8 @@ def image_plane_mesh_grid_from( For adaptive fitting, the KMeans object has been superseeded by the Hilbert object, which does not have this limitation and performs better in general. You should therefore consider using the Hilbert object instead. - """) + """ + ) sys.exit() diff --git a/autoarray/inversion/pixelization/mappers/mapper_grids.py b/autoarray/inversion/pixelization/mappers/mapper_grids.py index 8011ceb35..bfd3a9da2 100644 --- a/autoarray/inversion/pixelization/mappers/mapper_grids.py +++ b/autoarray/inversion/pixelization/mappers/mapper_grids.py @@ -5,10 +5,13 @@ if TYPE_CHECKING: from autoarray import Preloads +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__( @@ -69,3 +72,20 @@ def __init__( self.adapt_data = adapt_data self.preloads = preloads or Preloads() self.run_time_dict = run_time_dict + + @property + def image_plane_data_grid(self): + return self.source_plane_data_grid.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=self.image_plane_mesh_grid, + shape_native=self.source_plane_data_grid.mask.shape_native, + pixel_scales=self.source_plane_data_grid.mask.pixel_scales, + origin=self.source_plane_data_grid.mask.origin, + ) + + return Array2D( + values=mesh_pixels_per_image_pixels, mask=self.source_plane_data_grid.mask + ) diff --git a/autoarray/inversion/plot/inversion_plotters.py b/autoarray/inversion/plot/inversion_plotters.py index 8acf162bd..0f4f1796c 100644 --- a/autoarray/inversion/plot/inversion_plotters.py +++ b/autoarray/inversion/plot/inversion_plotters.py @@ -107,6 +107,7 @@ def figures_2d_of_pixelization( reconstruction: bool = False, errors: bool = False, regularization_weights: bool = False, + mesh_pixels_per_image_pixels: bool = False, zoom_to_brightest: bool = True, interpolate_to_uniform: bool = False, ): @@ -126,6 +127,9 @@ def figures_2d_of_pixelization( Whether to make a 2D plot (via `imshow` or `fill`) of the mapper's source-plane reconstruction. errors Whether to make a 2D plot (via `imshow` or `fill`) of the mapper's source-plane errors. + mesh_pixels_per_image_pixels + Whether to make a 2D plot (via `imshow`) of the number of image-mesh pixels per image pixels in the 2D + data's mask (only valid for pixelizations which use an `image_mesh`, e.g. Hilbert, KMeans). zoom_to_brightest For images not in the image-plane (e.g. the `plane_image`), whether to automatically zoom the plot to the brightest regions of the galaxies being plotted as opposed to the full extent of the grid. @@ -192,6 +196,20 @@ def figures_2d_of_pixelization( except TypeError: pass + if mesh_pixels_per_image_pixels: + mesh_pixels_per_image_pixels = ( + mapper_plotter.mapper.mapper_grids.mesh_pixels_per_image_pixels + ) + + self.mat_plot_2d.plot_array( + array=mesh_pixels_per_image_pixels, + visuals_2d=self.get_visuals_2d_for_data(), + auto_labels=AutoLabels( + title="Mesh Pixels Per Image Pixels", + filename="mesh_pixels_per_image_pixels", + ), + ) + # TODO : NEed to understand why this raises an error in voronoi_drawer. if regularization_weights: diff --git a/autoarray/plot/mat_plot/two_d.py b/autoarray/plot/mat_plot/two_d.py index df737b8bf..25894f525 100644 --- a/autoarray/plot/mat_plot/two_d.py +++ b/autoarray/plot/mat_plot/two_d.py @@ -58,7 +58,7 @@ def __init__( parallel_overscan_plot: Optional[w2d.ParallelOverscanPlot] = None, serial_prescan_plot: Optional[w2d.SerialPrescanPlot] = None, serial_overscan_plot: Optional[w2d.SerialOverscanPlot] = None, - use_log10 : bool = False + use_log10: bool = False, ): """ Visualizes 2D data structures (e.g an `Array2D`, `Grid2D`, `VectorField`, etc.) using Matplotlib. @@ -344,7 +344,11 @@ def plot_array( if self.colorbar is not False: cb = self.colorbar.set( - units=self.units, ax=ax, norm=norm, cb_unit=auto_labels.cb_unit, use_log10=self.use_log10 + units=self.units, + ax=ax, + norm=norm, + cb_unit=auto_labels.cb_unit, + use_log10=self.use_log10, ) self.colorbar_tickparams.set(cb=cb) diff --git a/autoarray/plot/wrap/base/cmap.py b/autoarray/plot/wrap/base/cmap.py index 19a4c5874..55232cde7 100644 --- a/autoarray/plot/wrap/base/cmap.py +++ b/autoarray/plot/wrap/base/cmap.py @@ -56,7 +56,7 @@ def vmax_from(self, array: np.ndarray): return np.max(array) return self.config_dict["vmax"] - def norm_from(self, array: np.ndarray, use_log10 : bool = False) -> object: + def norm_from(self, array: np.ndarray, use_log10: bool = False) -> object: """ Returns the `Normalization` object which scales of the colormap. diff --git a/autoarray/plot/wrap/base/colorbar.py b/autoarray/plot/wrap/base/colorbar.py index 06ee3293e..491564978 100644 --- a/autoarray/plot/wrap/base/colorbar.py +++ b/autoarray/plot/wrap/base/colorbar.py @@ -60,7 +60,7 @@ def cb_unit(self): return conf.instance["visualize"]["general"]["units"]["cb_unit"] return self.manual_unit - def tick_values_from(self, norm=None, use_log10 : bool = False): + def tick_values_from(self, norm=None, use_log10: bool = False): if ( sum( x is not None @@ -76,23 +76,23 @@ def tick_values_from(self, norm=None, use_log10 : bool = False): return self.manual_tick_values if norm is not None: - min_value = norm.vmin max_value = norm.vmax if use_log10: - log_mid_value = (np.log10(max_value) + np.log10(min_value)) / 2.0 - mid_value = 10 ** log_mid_value + mid_value = 10**log_mid_value else: - mid_value = (max_value + min_value) / 2.0 return [min_value, mid_value, max_value] def tick_labels_from( - self, units: Units, manual_tick_values: List[float], cb_unit=None, + self, + units: Units, + manual_tick_values: List[float], + cb_unit=None, ): if manual_tick_values is None: return None @@ -107,7 +107,6 @@ def tick_labels_from( ] if self.manual_log10: - manual_tick_labels = [ "{:.0e}".format(label) for label in manual_tick_labels ] @@ -134,14 +133,18 @@ def tick_labels_from( return manual_tick_labels - def set(self, units: Units, ax=None, norm=None, cb_unit=None, use_log10 : bool = False): + def set( + self, units: Units, ax=None, norm=None, cb_unit=None, use_log10: bool = False + ): """ Set the figure's colorbar, optionally overriding the tick labels and values with manual inputs. """ tick_values = self.tick_values_from(norm=norm, use_log10=use_log10) tick_labels = self.tick_labels_from( - manual_tick_values=tick_values, units=units, cb_unit=cb_unit, + manual_tick_values=tick_values, + units=units, + cb_unit=cb_unit, ) if tick_values is None and tick_labels is None: diff --git a/autoarray/plot/wrap/base/ticks.py b/autoarray/plot/wrap/base/ticks.py index a6c9f89c4..f18499917 100644 --- a/autoarray/plot/wrap/base/ticks.py +++ b/autoarray/plot/wrap/base/ticks.py @@ -133,8 +133,6 @@ def tick_values_rounded(self): @property def labels_linear(self): - - if self.units.use_raw: return self.with_appended_suffix(self.tick_values_rounded) @@ -441,7 +439,7 @@ def set( yunit=xunit, use_integers=use_integers, is_for_1d_plot=is_for_1d_plot, - is_log10=is_log10 + is_log10=is_log10, ) plt.xticks(ticks=ticks, labels=labels, **self.config_dict) diff --git a/autoarray/plot/wrap/base/title.py b/autoarray/plot/wrap/base/title.py index b5a8e822c..78619012a 100644 --- a/autoarray/plot/wrap/base/title.py +++ b/autoarray/plot/wrap/base/title.py @@ -19,8 +19,7 @@ def __init__(self, **kwargs): self.manual_label = self.kwargs.get("label") - def set(self, auto_title=None, use_log10 : bool = False): - + def set(self, auto_title=None, use_log10: bool = False): config_dict = self.config_dict label = auto_title if self.manual_label is None else self.manual_label diff --git a/autoarray/plot/wrap/one_d/yx_plot.py b/autoarray/plot/wrap/one_d/yx_plot.py index 393af5ef6..e8671f5b4 100644 --- a/autoarray/plot/wrap/one_d/yx_plot.py +++ b/autoarray/plot/wrap/one_d/yx_plot.py @@ -81,6 +81,5 @@ def plot_y_vs_x( "{semilogy, loglog})" ) - if y_extra is not None: plt.plot(x, y_extra, c="r") diff --git a/autoarray/plot/wrap/two_d/contour.py b/autoarray/plot/wrap/two_d/contour.py index fa1f22879..d03b66b67 100644 --- a/autoarray/plot/wrap/two_d/contour.py +++ b/autoarray/plot/wrap/two_d/contour.py @@ -65,7 +65,12 @@ def levels_from( return self.manual_levels - def set(self, array: Union[np.ndarray, Array2D], extent: List[float] = None, use_log10 : bool = False): + def set( + self, + array: Union[np.ndarray, Array2D], + extent: List[float] = None, + use_log10: bool = False, + ): """ Plot an input grid of (y,x) coordinates using the matplotlib method `plt.scatter`. diff --git a/autoarray/structures/grids/grid_2d_util.py b/autoarray/structures/grids/grid_2d_util.py index 2d50ebc05..0d533aa87 100644 --- a/autoarray/structures/grids/grid_2d_util.py +++ b/autoarray/structures/grids/grid_2d_util.py @@ -849,3 +849,43 @@ def compute_polygon_area(points): y = points[:, 0] return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) + + +# @numba_util.jit() +def grid_pixels_in_mask_pixels_from( + grid, shape_native, pixel_scales, origin +) -> np.ndarray: + """ + Returns an array containing the number of pixels of one grid in every pixel of another masked grid. + + For example, image-mesh adaption may be performed on a 3.0" circular mask of data. The high weight pixels + may have 3 or more mesh pixels per image pixel, whereas low weight regions may have zero pixels. The array + returned by this function gives the integer number of pixels in each data pixel. + + Parameters + ---------- + grid_pixel_centres + The 2D integer index of every image pixel that each image-mesh pixel falls within. + shape_native + The 2D shape of the data's mask, which the number of image-mesh pixels that fall within eac pixel is counted. + + Returns + ------- + An array containing the integer number of image-mesh pixels that fall without each of the data's mask. + """ + grid_pixel_centres = geometry_util.grid_pixel_centres_2d_slim_from( + grid_scaled_2d_slim=grid, + shape_native=shape_native, + pixel_scales=pixel_scales, + origin=origin, + ).astype("int") + + mesh_pixels_per_image_pixel = np.zeros(shape=shape_native) + + for i in range(grid_pixel_centres.shape[0]): + y = grid_pixel_centres[i, 0] + x = grid_pixel_centres[i, 1] + + mesh_pixels_per_image_pixel[y, x] += 1 + + return mesh_pixels_per_image_pixel diff --git a/autoarray/structures/plot/structure_plotters.py b/autoarray/structures/plot/structure_plotters.py index 17fcd8372..79c9e3d26 100644 --- a/autoarray/structures/plot/structure_plotters.py +++ b/autoarray/structures/plot/structure_plotters.py @@ -134,7 +134,7 @@ def __init__( should_plot_grid: bool = False, should_plot_zero: bool = False, plot_axis_type: Optional[str] = None, - plot_yx_dict=None + plot_yx_dict=None, ): """ Plots two 1D objects using the matplotlib method `plot()` (or a similar method) and many other matplotlib diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py index 54ef7267a..8960f727e 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py @@ -3,8 +3,8 @@ import autoarray as aa -def test__mesh_pixels_per_image_pixels_from(): +def test__mesh_pixels_per_image_pixels_from(): mask = aa.Mask2D.circular( shape_native=(3, 3), radius=2.0, @@ -21,12 +21,9 @@ def test__mesh_pixels_per_image_pixels_from(): image_mesh = aa.image_mesh.Hilbert(pixels=8) mesh_pixels_per_image_pixels = image_mesh.mesh_pixels_per_image_pixels_from( - grid=grid, - mesh_grid=mesh_grid + grid=grid, mesh_grid=mesh_grid ) - assert mesh_pixels_per_image_pixels.native == pytest.approx(np.array( - [[0, 0, 0], - [0, 3, 0], - [1, 0 ,0]] - ), 1.0e-4) \ No newline at end of file + assert mesh_pixels_per_image_pixels.native == pytest.approx( + np.array([[0, 0, 0], [0, 3, 0], [1, 0, 0]]), 1.0e-4 + ) diff --git a/test_autoarray/inversion/plot/test_inversion_plotters.py b/test_autoarray/inversion/plot/test_inversion_plotters.py index 2f470212b..c7a07b24e 100644 --- a/test_autoarray/inversion/plot/test_inversion_plotters.py +++ b/test_autoarray/inversion/plot/test_inversion_plotters.py @@ -38,12 +38,14 @@ def test__individual_attributes_are_output_for_all_mappers( reconstructed_image=True, reconstruction=True, errors=True, + mesh_pixels_per_image_pixels=True, regularization_weights=True, ) assert path.join(plot_path, "reconstructed_image.png") in plot_patch.paths assert path.join(plot_path, "reconstruction.png") in plot_patch.paths assert path.join(plot_path, "errors.png") in plot_patch.paths + assert path.join(plot_path, "mesh_pixels_per_image_pixels.png") in plot_patch.paths assert path.join(plot_path, "regularization_weights.png") in plot_patch.paths plot_patch.paths = [] From 48c5858e1a5e2d6e43cccdc4b0e01f44f1fe08aa Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 14 Jan 2024 13:13:07 +0000 Subject: [PATCH 3/5] inverison plotter now plots mesh_pixels_per_image_pixels --- autoarray/config/visualize/include.yaml | 2 +- autoarray/config/visualize/plots.yaml | 9 ++-- .../pixelization/mappers/mapper_grids.py | 3 +- .../inversion/plot/inversion_plotters.py | 53 ++++++++++++++----- test_autoarray/config/visualize.yaml | 4 +- .../inversion/plot/test_inversion_plotters.py | 4 +- 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/autoarray/config/visualize/include.yaml b/autoarray/config/visualize/include.yaml index d0f653ae7..270b6b2f4 100644 --- a/autoarray/config/visualize/include.yaml +++ b/autoarray/config/visualize/include.yaml @@ -9,7 +9,7 @@ include_1d: include_2d: border: true # Include the border of the mask (all pixels on the outside of the mask) ? grid: false # Include the data's 2D grid of (y,x) coordinates ? - mapper_image_plane_mesh_grid: true # For an Inversion, include the pixel centres computed in the image-plane / data frame? + mapper_image_plane_mesh_grid: false # For an Inversion, include the pixel centres computed in the image-plane / data frame? mapper_source_plane_data_grid: false # For an Inversion, include the centres of the image-plane grid mapped to the source-plane / frame in source-plane figures? mapper_source_plane_mesh_grid: false # For an Inversion, include the centres of the mesh pixels in the source-plane / source-plane? mask: true # Include a mask ? diff --git a/autoarray/config/visualize/plots.yaml b/autoarray/config/visualize/plots.yaml index 34df67863..961cd8a41 100644 --- a/autoarray/config/visualize/plots.yaml +++ b/autoarray/config/visualize/plots.yaml @@ -29,10 +29,11 @@ inversion: # Settings for plots of inversions (e all_at_end_png: true # Plot all individual plots listed below as .png (even if False)? all_at_end_fits: true # Plot all individual plots listed below as .fits (even if False)? all_at_end_pdf: false # Plot all individual plots listed below as publication-quality .pdf (even if False)? - errors: false - reconstructed_image: false - reconstruction: false - regularization_weights: false + errors: false # Plot image of the errors of every mesh-pixel reconstructed value? + mesh_pixels_per_image_pixels : false # Plot the number of image-plane mesh pixels per masked data pixels? + reconstructed_image: false # Plot image of the reconstructed data (e.g. in the image-plane)? + reconstruction: false # Plot the reconstructed inversion (e.g. the pixelization's mesh in the source-plane)? + regularization_weights: false # Plot the effective regularization weight of every inversion mesh pixel? interferometer: # Settings for plots of interferometer datasets (e.g. InterferometerPlotter). amplitudes_vs_uv_distances: false phases_vs_uv_distances: false diff --git a/autoarray/inversion/pixelization/mappers/mapper_grids.py b/autoarray/inversion/pixelization/mappers/mapper_grids.py index bfd3a9da2..d27dd11a6 100644 --- a/autoarray/inversion/pixelization/mappers/mapper_grids.py +++ b/autoarray/inversion/pixelization/mappers/mapper_grids.py @@ -87,5 +87,6 @@ def mesh_pixels_per_image_pixels(self): ) return Array2D( - values=mesh_pixels_per_image_pixels, mask=self.source_plane_data_grid.mask + values=mesh_pixels_per_image_pixels, + mask=self.source_plane_data_grid.derive_mask.sub_1, ) diff --git a/autoarray/inversion/plot/inversion_plotters.py b/autoarray/inversion/plot/inversion_plotters.py index 0f4f1796c..d3eb7cad6 100644 --- a/autoarray/inversion/plot/inversion_plotters.py +++ b/autoarray/inversion/plot/inversion_plotters.py @@ -197,18 +197,21 @@ def figures_2d_of_pixelization( pass if mesh_pixels_per_image_pixels: - mesh_pixels_per_image_pixels = ( - mapper_plotter.mapper.mapper_grids.mesh_pixels_per_image_pixels - ) + try: + mesh_pixels_per_image_pixels = ( + mapper_plotter.mapper.mapper_grids.mesh_pixels_per_image_pixels + ) - self.mat_plot_2d.plot_array( - array=mesh_pixels_per_image_pixels, - visuals_2d=self.get_visuals_2d_for_data(), - auto_labels=AutoLabels( - title="Mesh Pixels Per Image Pixels", - filename="mesh_pixels_per_image_pixels", - ), - ) + self.mat_plot_2d.plot_array( + array=mesh_pixels_per_image_pixels, + visuals_2d=self.get_visuals_2d_for_data(), + auto_labels=AutoLabels( + title="Mesh Pixels Per Image Pixels", + filename="mesh_pixels_per_image_pixels", + ), + ) + except Exception: + pass # TODO : NEed to understand why this raises an error in voronoi_drawer. @@ -239,11 +242,27 @@ def subplot_of_mapper( auto_filename The default filename of the output subplot if written to hard-disk. """ - self.open_subplot_figure(number_subplots=6) + self.open_subplot_figure(number_subplots=9) + + mapper_image_plane_mesh_grid = self.include_2d._mapper_image_plane_mesh_grid + + self.include_2d._mapper_image_plane_mesh_grid = False + self.figures_2d_of_pixelization( + pixelization_index=mapper_index, reconstructed_image=True + ) + self.include_2d._mapper_image_plane_mesh_grid = True self.figures_2d_of_pixelization( pixelization_index=mapper_index, reconstructed_image=True ) + + self.include_2d._mapper_image_plane_mesh_grid = False + self.figures_2d_of_pixelization( + pixelization_index=mapper_index, mesh_pixels_per_image_pixels=True + ) + + self.include_2d._mapper_image_plane_mesh_grid = mapper_image_plane_mesh_grid + self.figures_2d_of_pixelization( pixelization_index=mapper_index, reconstruction=True ) @@ -268,6 +287,16 @@ def subplot_of_mapper( self.figures_2d_of_pixelization( pixelization_index=mapper_index, errors=True, zoom_to_brightest=False ) + + self.set_title(label="Regularization Weights (Unzoomed)") + try: + self.figures_2d_of_pixelization( + pixelization_index=mapper_index, + regularization_weights=True, + zoom_to_brightest=False, + ) + except IndexError: + pass self.set_title(label=None) self.mat_plot_2d.output.subplot_to_figure( diff --git a/test_autoarray/config/visualize.yaml b/test_autoarray/config/visualize.yaml index 6f0d2c6de..568a11349 100644 --- a/test_autoarray/config/visualize.yaml +++ b/test_autoarray/config/visualize.yaml @@ -6,7 +6,7 @@ general: disable_zoom_for_fits: true # If True, the zoom-in around the masked region is disabled when outputting .fits files, which is useful to retain the same dimensions as the input data. include_2d: border: true - mapper_image_plane_mesh_grid: true + mapper_image_plane_mesh_grid: false mapper_source_plane_data_grid: false mapper_source_plane_mesh_grid: false mask: true @@ -34,7 +34,7 @@ include: origin: false include_2d: border: true - mapper_image_plane_mesh_grid: true + mapper_image_plane_mesh_grid: false mapper_source_plane_data_grid: false mapper_source_plane_mesh_grid: false mask: true diff --git a/test_autoarray/inversion/plot/test_inversion_plotters.py b/test_autoarray/inversion/plot/test_inversion_plotters.py index c7a07b24e..4e9725049 100644 --- a/test_autoarray/inversion/plot/test_inversion_plotters.py +++ b/test_autoarray/inversion/plot/test_inversion_plotters.py @@ -38,14 +38,12 @@ def test__individual_attributes_are_output_for_all_mappers( reconstructed_image=True, reconstruction=True, errors=True, - mesh_pixels_per_image_pixels=True, regularization_weights=True, ) assert path.join(plot_path, "reconstructed_image.png") in plot_patch.paths assert path.join(plot_path, "reconstruction.png") in plot_patch.paths assert path.join(plot_path, "errors.png") in plot_patch.paths - assert path.join(plot_path, "mesh_pixels_per_image_pixels.png") in plot_patch.paths assert path.join(plot_path, "regularization_weights.png") in plot_patch.paths plot_patch.paths = [] @@ -60,12 +58,14 @@ def test__individual_attributes_are_output_for_all_mappers( pixelization_index=0, reconstructed_image=True, reconstruction=True, + mesh_pixels_per_image_pixels=True, errors=True, regularization_weights=True, ) assert path.join(plot_path, "reconstructed_image.png") in plot_patch.paths assert path.join(plot_path, "reconstruction.png") in plot_patch.paths + assert path.join(plot_path, "mesh_pixels_per_image_pixels.png") in plot_patch.paths assert path.join(plot_path, "errors.png") in plot_patch.paths assert path.join(plot_path, "regularization_weights.png") in plot_patch.paths From 70765caf6434a8d00af471af9072b89abbb13531 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 14 Jan 2024 13:40:05 +0000 Subject: [PATCH 4/5] exception raised if too few image pixels clustered added --- autoarray/inversion/inversion/settings.py | 4 ++ autoarray/inversion/mock/mock_image_mesh.py | 2 +- .../pixelization/image_mesh/abstract.py | 52 ++++++++++++++++++- .../image_mesh/abstract_weighted.py | 2 +- .../pixelization/image_mesh/hilbert.py | 9 +++- .../image_mesh/hilbert_balanced.py | 2 +- .../pixelization/image_mesh/kmeans.py | 2 +- .../pixelization/image_mesh/overlay.py | 2 +- .../pixelization/image_mesh/test_abstract.py | 47 ++++++++++++++++- .../pixelization/image_mesh/test_hilbert.py | 1 + 10 files changed, 114 insertions(+), 9 deletions(-) diff --git a/autoarray/inversion/inversion/settings.py b/autoarray/inversion/inversion/settings.py index 7c2dc8b76..ad0477b91 100644 --- a/autoarray/inversion/inversion/settings.py +++ b/autoarray/inversion/inversion/settings.py @@ -21,6 +21,8 @@ def __init__( use_w_tilde_numpy: bool = False, use_source_loop: bool = False, use_linear_operators: bool = False, + image_mesh_min_mesh_pixels_per_pixel = None, + image_mesh_min_mesh_number : int = 5, tolerance: float = 1e-8, maxiter: int = 250, ): @@ -72,6 +74,8 @@ def __init__( self._no_regularization_add_to_curvature_diag_value = ( no_regularization_add_to_curvature_diag_value ) + self.image_mesh_min_mesh_pixels_per_pixel = image_mesh_min_mesh_pixels_per_pixel + self.image_mesh_min_mesh_number = image_mesh_min_mesh_number self.tolerance = tolerance self.maxiter = maxiter self.use_w_tilde_numpy = use_w_tilde_numpy diff --git a/autoarray/inversion/mock/mock_image_mesh.py b/autoarray/inversion/mock/mock_image_mesh.py index 5ee5fece4..c834a8d01 100644 --- a/autoarray/inversion/mock/mock_image_mesh.py +++ b/autoarray/inversion/mock/mock_image_mesh.py @@ -14,7 +14,7 @@ def __init__(self, image_plane_mesh_grid=None): self.image_plane_mesh_grid = image_plane_mesh_grid def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] + self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings = None ) -> Grid2DIrregular: if adapt_data is not None and self.image_plane_mesh_grid is not None: return adapt_data * self.image_plane_mesh_grid diff --git a/autoarray/inversion/pixelization/image_mesh/abstract.py b/autoarray/inversion/pixelization/image_mesh/abstract.py index edb8d8f0d..19e70deca 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract.py +++ b/autoarray/inversion/pixelization/image_mesh/abstract.py @@ -8,6 +8,7 @@ from autoarray.structures.grids import grid_2d_util +from autoarray import exc class AbstractImageMesh: def __init__(self): @@ -48,7 +49,7 @@ def weight_map_from(self, adapt_data: np.ndarray): return weight_map def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings = None ) -> Grid2DIrregular: raise NotImplementedError @@ -84,3 +85,52 @@ def mesh_pixels_per_image_pixels_from( ) return Array2D(values=mesh_pixels_per_image_pixels, mask=grid.mask) + + def check_mesh_pixels_per_image_pixels(self, grid, mesh_grid, settings): + """ + Checks the number of mesh pixels in every image pixel and raises an `InversionException` if there are fewer + mesh pixels inside a certain number of image-pixels than the input settings. + + This allows a user to force a model-fit to use image-mesh's which cluster a large number of mesh pixels to + the brightest regions of the image data (E.g. the highst weighted regions). + + The check works as follows: + + 1) Compute the 2D array of the number of mesh pixels in every masked data image pixel. + 2) Find the number of mesh pixels in the N data pixels with the larger number of mesh pixels, where N is + given by `settings.image_mesh_min_mesh_number`. For example, if `settings.image_mesh_min_mesh_number=5` then + the number of mesh pixels in the 5 data pixels with the most data pixels is computed. + 3) Compare the lowest value above to the value `settings.image_mesh_min_mesh_pixels_per_pixel`. If the value is + below this value, raise an `InversionException`. + + Therefore, by settings `settings.image_mesh_min_mesh_pixels_per_pixel` to a value above 1 the code is forced + to adapt the image mesh enough to put many mesh pixels in the brightest image pixels. + + Parameters + ---------- + grid + The masked (y,x) grid of the data coordinates, corresponding to the mask applied to the data. The number of + mesh pixels mapped inside each of this grid's image-pixels is returned. + mesh_grid + The image mesh-grid computed by the class which adapts to the data's mask. The number of image mesh pixels + that fall within each of the data's mask pixels is returned. + settings + The inversion settings, which have the criteria dictating if the image-mesh has clustered enough or if + an exception is raised. + """ + + if settings is not None: + if settings.image_mesh_min_mesh_pixels_per_pixel is not None: + + mesh_pixels_per_image_pixels = self.mesh_pixels_per_image_pixels_from( + grid=grid, + mesh_grid=mesh_grid + ) + + indices_of_highest_values = np.argsort(mesh_pixels_per_image_pixels)[-settings.image_mesh_min_mesh_number:] + lowest_mesh_pixels = np.min(mesh_pixels_per_image_pixels[indices_of_highest_values]) + + if lowest_mesh_pixels < settings.image_mesh_min_mesh_pixels_per_pixel: + raise exc.InversionException() + + return mesh_grid \ No newline at end of file diff --git a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py b/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py index 7f6d76d61..46ab1b9d2 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py +++ b/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py @@ -70,6 +70,6 @@ def weight_map_from(self, adapt_data: np.ndarray): return weight_map def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings = None ) -> Grid2DIrregular: raise NotImplementedError diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert.py b/autoarray/inversion/pixelization/image_mesh/hilbert.py index a9635655e..39b88b202 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert.py @@ -8,6 +8,7 @@ from autoarray.inversion.pixelization.image_mesh.abstract_weighted import ( AbstractImageMeshWeighted, ) +from autoarray.inversion.inversion.settings import SettingsInversion from autoarray.structures.grids.irregular_2d import Grid2DIrregular from autoarray import exc @@ -245,7 +246,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], settings : SettingsInversion = None ) -> Grid2DIrregular: """ Returns an image mesh by running the Hilbert curve on the weight map. @@ -298,4 +299,10 @@ def image_plane_mesh_grid_from( mesh_grid = Grid2DIrregular(values=np.stack((drawn_y, drawn_x), axis=-1)) + self.check_mesh_pixels_per_image_pixels( + grid=grid, + mesh_grid=mesh_grid, + settings=settings + ) + return mesh_grid diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py index b895bbe11..e96deffd1 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py @@ -71,7 +71,7 @@ def __init__( self.ratio = ratio def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] + self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings = None ) -> Grid2DIrregular: """ Returns an image mesh by running the balanced Hilbert curve on the weight map. diff --git a/autoarray/inversion/pixelization/image_mesh/kmeans.py b/autoarray/inversion/pixelization/image_mesh/kmeans.py index c9c213de3..4f051d035 100644 --- a/autoarray/inversion/pixelization/image_mesh/kmeans.py +++ b/autoarray/inversion/pixelization/image_mesh/kmeans.py @@ -51,7 +51,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], settings = None ) -> Grid2DIrregular: """ Returns an image mesh by running a KMeans clustering algorithm on the weight map. diff --git a/autoarray/inversion/pixelization/image_mesh/overlay.py b/autoarray/inversion/pixelization/image_mesh/overlay.py index 33f2aa5a2..e616cf904 100644 --- a/autoarray/inversion/pixelization/image_mesh/overlay.py +++ b/autoarray/inversion/pixelization/image_mesh/overlay.py @@ -185,7 +185,7 @@ def __init__(self, shape=(3, 3)): self.shape = (int(shape[0]), int(shape[1])) def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings = None ) -> Grid2DIrregular: """ Returns an image-mesh by overlaying a uniform grid of (y,x) coordinates over the masked image that the diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py index 8960f727e..15ee1e415 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py @@ -15,7 +15,7 @@ def test__mesh_pixels_per_image_pixels_from(): grid = aa.Grid2D.from_mask(mask=mask) mesh_grid = aa.Grid2DIrregular( - values=[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (-1.0, -1.0)] + values=[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 1.0), (0.0, 1.0), (-1.0, -1.0)] ) image_mesh = aa.image_mesh.Hilbert(pixels=8) @@ -25,5 +25,48 @@ def test__mesh_pixels_per_image_pixels_from(): ) assert mesh_pixels_per_image_pixels.native == pytest.approx( - np.array([[0, 0, 0], [0, 3, 0], [1, 0, 0]]), 1.0e-4 + np.array([[0, 0, 0], [0, 3, 2], [1, 0, 0]]), 1.0e-4 ) + +def test__check_mesh_pixels_per_image_pixels(): + + mask = aa.Mask2D.circular( + shape_native=(3, 3), + radius=2.0, + pixel_scales=1.0, + sub_size=1, + ) + + grid = aa.Grid2D.from_mask(mask=mask) + + mesh_grid = aa.Grid2DIrregular( + values=[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 1.0), (0.0, 1.0), (-1.0, -1.0)] + ) + + image_mesh = aa.image_mesh.Hilbert(pixels=8) + + image_mesh.check_mesh_pixels_per_image_pixels( + grid=grid, + mesh_grid=mesh_grid, + settings=None + ) + + image_mesh.check_mesh_pixels_per_image_pixels( + grid=grid, + mesh_grid=mesh_grid, + settings=aa.SettingsInversion(image_mesh_min_mesh_pixels_per_pixel=3, image_mesh_min_mesh_number=1) + ) + + with pytest.raises(aa.exc.InversionException): + image_mesh.check_mesh_pixels_per_image_pixels( + grid=grid, + mesh_grid=mesh_grid, + settings=aa.SettingsInversion(image_mesh_min_mesh_pixels_per_pixel=5, image_mesh_min_mesh_number=1) + ) + + with pytest.raises(aa.exc.InversionException): + image_mesh.check_mesh_pixels_per_image_pixels( + grid=grid, + mesh_grid=mesh_grid, + settings=aa.SettingsInversion(image_mesh_min_mesh_pixels_per_pixel=3, image_mesh_min_mesh_number=2) + ) \ No newline at end of file diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py index 3286f4a2e..8b5f44acc 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py @@ -25,3 +25,4 @@ def test__image_plane_mesh_grid_from(): [-1.02590674, -1.70984456], 1.0e-4, ) + From 51d87a015e5851a5f7f4126cf0c86fc9b51c827f Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 14 Jan 2024 14:51:07 +0000 Subject: [PATCH 5/5] check_adapt_background_pixels imeplmented --- autoarray/inversion/inversion/settings.py | 26 +++++- autoarray/inversion/mock/mock_image_mesh.py | 2 +- .../pixelization/image_mesh/abstract.py | 82 ++++++++++++++-- .../image_mesh/abstract_weighted.py | 2 +- .../pixelization/image_mesh/hilbert.py | 13 ++- .../image_mesh/hilbert_balanced.py | 2 +- .../pixelization/image_mesh/kmeans.py | 2 +- .../pixelization/image_mesh/overlay.py | 2 +- .../pixelization/image_mesh/test_abstract.py | 93 +++++++++++++------ .../pixelization/image_mesh/test_hilbert.py | 1 - 10 files changed, 177 insertions(+), 48 deletions(-) diff --git a/autoarray/inversion/inversion/settings.py b/autoarray/inversion/inversion/settings.py index ad0477b91..7b78891c4 100644 --- a/autoarray/inversion/inversion/settings.py +++ b/autoarray/inversion/inversion/settings.py @@ -21,8 +21,10 @@ def __init__( use_w_tilde_numpy: bool = False, use_source_loop: bool = False, use_linear_operators: bool = False, - image_mesh_min_mesh_pixels_per_pixel = None, - image_mesh_min_mesh_number : int = 5, + image_mesh_min_mesh_pixels_per_pixel=None, + image_mesh_min_mesh_number: int = 5, + image_mesh_adapt_background_percent_threshold: float = None, + image_mesh_adapt_background_percent_check: float = 0.1, tolerance: float = 1e-8, maxiter: int = 250, ): @@ -55,6 +57,19 @@ def __init__( use_linear_operators For an interferometer inversion, whether to use the linear operator solution to solve the linear system or not (this input does nothing for dataset data). + image_mesh_min_mesh_pixels_per_pixel + If not None, the image-mesh must place this many mesh pixels per image pixels in the N highest weighted + regions of the adapt data, or an `InversionException` is raised. This can be used to force the image-mesh + to cluster large numbers of source pixels to the adapt-datas brightest regions. + image_mesh_min_mesh_number + The value N given above in the docstring for `image_mesh_min_mesh_pixels_per_pixel`, indicating how many + image pixels are checked for having a threshold number of mesh pixels. + image_mesh_adapt_background_percent_threshold + If not None, the image-mesh must place this percentage of mesh-pixels in the background regions of the + `adapt_data`, where the background is the `image_mesh_adapt_background_percent_check` masked data pixels + with the lowest values. + image_mesh_adapt_background_percent_check + The percentage of masked data pixels which are checked for the background criteria. 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). @@ -76,6 +91,13 @@ def __init__( ) self.image_mesh_min_mesh_pixels_per_pixel = image_mesh_min_mesh_pixels_per_pixel self.image_mesh_min_mesh_number = image_mesh_min_mesh_number + self.image_mesh_adapt_background_percent_threshold = ( + image_mesh_adapt_background_percent_threshold + ) + self.image_mesh_adapt_background_percent_check = ( + image_mesh_adapt_background_percent_check + ) + self.tolerance = tolerance self.maxiter = maxiter self.use_w_tilde_numpy = use_w_tilde_numpy diff --git a/autoarray/inversion/mock/mock_image_mesh.py b/autoarray/inversion/mock/mock_image_mesh.py index c834a8d01..6c263bf6d 100644 --- a/autoarray/inversion/mock/mock_image_mesh.py +++ b/autoarray/inversion/mock/mock_image_mesh.py @@ -14,7 +14,7 @@ def __init__(self, image_plane_mesh_grid=None): self.image_plane_mesh_grid = image_plane_mesh_grid def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings=None ) -> Grid2DIrregular: if adapt_data is not None and self.image_plane_mesh_grid is not None: return adapt_data * self.image_plane_mesh_grid diff --git a/autoarray/inversion/pixelization/image_mesh/abstract.py b/autoarray/inversion/pixelization/image_mesh/abstract.py index 19e70deca..d09cedaf1 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract.py +++ b/autoarray/inversion/pixelization/image_mesh/abstract.py @@ -1,5 +1,6 @@ from typing import Optional +import copy import numpy as np from autoarray.structures.arrays.uniform_2d import Array2D @@ -10,6 +11,7 @@ from autoarray import exc + class AbstractImageMesh: def __init__(self): """ @@ -49,7 +51,7 @@ def weight_map_from(self, adapt_data: np.ndarray): return weight_map def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings=None ) -> Grid2DIrregular: raise NotImplementedError @@ -102,7 +104,7 @@ def check_mesh_pixels_per_image_pixels(self, grid, mesh_grid, settings): the number of mesh pixels in the 5 data pixels with the most data pixels is computed. 3) Compare the lowest value above to the value `settings.image_mesh_min_mesh_pixels_per_pixel`. If the value is below this value, raise an `InversionException`. - + Therefore, by settings `settings.image_mesh_min_mesh_pixels_per_pixel` to a value above 1 the code is forced to adapt the image mesh enough to put many mesh pixels in the brightest image pixels. @@ -121,16 +123,80 @@ def check_mesh_pixels_per_image_pixels(self, grid, mesh_grid, settings): if settings is not None: if settings.image_mesh_min_mesh_pixels_per_pixel is not None: - mesh_pixels_per_image_pixels = self.mesh_pixels_per_image_pixels_from( - grid=grid, - mesh_grid=mesh_grid + grid=grid, mesh_grid=mesh_grid ) - indices_of_highest_values = np.argsort(mesh_pixels_per_image_pixels)[-settings.image_mesh_min_mesh_number:] - lowest_mesh_pixels = np.min(mesh_pixels_per_image_pixels[indices_of_highest_values]) + indices_of_highest_values = np.argsort(mesh_pixels_per_image_pixels)[ + -settings.image_mesh_min_mesh_number : + ] + lowest_mesh_pixels = np.min( + mesh_pixels_per_image_pixels[indices_of_highest_values] + ) if lowest_mesh_pixels < settings.image_mesh_min_mesh_pixels_per_pixel: raise exc.InversionException() - return mesh_grid \ No newline at end of file + return mesh_grid + + def check_adapt_background_pixels(self, grid, mesh_grid, adapt_data, settings): + """ + Checks the number of mesh pixels in the background of the image-mesh and raises an `InversionException` if + there are fewer mesh pixels in the background than the input settings. + + This allows a user to force a model-fit to use image-mesh's which cluster a minimum number of mesh pixels to + the faintest regions of the image data (E.g. the lowest weighted regions). This prevents too few image-mesh + pixels being allocated to the background of the data. + + The check works as follows: + + 1) Find all pixels in the background of the `adapt_data`, which are N pixels with the lowest values, where N is + a percentage given by `settings.image_mesh_adapt_background_percent_check`. If N is 50%, then the half of + pixels in `adapt_data` with the lowest values will be checked. + 2) Sum the total number of mesh pixels in these background pixels, thereby estimating the number of mesh pixels + assigned to background pixels. + 3) Compare this value to the total number of mesh pixels multiplied + by `settings.image_mesh_adapt_background_percent_threshold` and raise an `InversionException` if the number + of mesh pixels is below this value, meaning the background did not have sufficient mesh pixels in it. + + Therefore, by setting `settings.image_mesh_adapt_background_percent_threshold` the code is forced + to adapt the image mesh in a way that places many mesh pixels in the background regions. + + Parameters + ---------- + grid + The masked (y,x) grid of the data coordinates, corresponding to the mask applied to the data. The number of + mesh pixels mapped inside each of this grid's image-pixels is returned. + mesh_grid + The image mesh-grid computed by the class which adapts to the data's mask. The number of image mesh pixels + that fall within each of the data's mask pixels is returned. + adapt_data + A image which represents one or more components in the masked 2D data in the image-plane. + settings + The inversion settings, which have the criteria dictating if the image-mesh has clustered enough or if + an exception is raised. + """ + if settings is not None: + if settings.image_mesh_adapt_background_percent_threshold is not None: + pixels = mesh_grid.shape[0] + + pixels_in_background = int( + grid.shape[0] * settings.image_mesh_adapt_background_percent_check + ) + + indices_of_lowest_values = np.argsort(adapt_data)[:pixels_in_background] + mask_background = np.zeros_like(adapt_data, dtype=bool) + mask_background[indices_of_lowest_values] = True + + mesh_pixels_per_image_pixels = self.mesh_pixels_per_image_pixels_from( + grid=grid, mesh_grid=mesh_grid + ) + + mesh_pixels_in_background = sum( + mesh_pixels_per_image_pixels[mask_background] + ) + + if mesh_pixels_in_background < ( + pixels * settings.image_mesh_adapt_background_percent_threshold + ): + raise exc.InversionException() diff --git a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py b/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py index 46ab1b9d2..ece8c25de 100644 --- a/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py +++ b/autoarray/inversion/pixelization/image_mesh/abstract_weighted.py @@ -70,6 +70,6 @@ def weight_map_from(self, adapt_data: np.ndarray): return weight_map def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings=None ) -> Grid2DIrregular: raise NotImplementedError diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert.py b/autoarray/inversion/pixelization/image_mesh/hilbert.py index 39b88b202..9e43fbb23 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert.py @@ -246,7 +246,10 @@ def __init__( ) def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings : SettingsInversion = None + self, + grid: Grid2D, + adapt_data: Optional[np.ndarray], + settings: SettingsInversion = None, ) -> Grid2DIrregular: """ Returns an image mesh by running the Hilbert curve on the weight map. @@ -300,9 +303,11 @@ def image_plane_mesh_grid_from( mesh_grid = Grid2DIrregular(values=np.stack((drawn_y, drawn_x), axis=-1)) self.check_mesh_pixels_per_image_pixels( - grid=grid, - mesh_grid=mesh_grid, - settings=settings + grid=grid, mesh_grid=mesh_grid, settings=settings + ) + + self.check_adapt_background_pixels( + grid=grid, mesh_grid=mesh_grid, adapt_data=adapt_data, settings=settings ) return mesh_grid diff --git a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py index e96deffd1..49740349b 100644 --- a/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py +++ b/autoarray/inversion/pixelization/image_mesh/hilbert_balanced.py @@ -71,7 +71,7 @@ def __init__( self.ratio = ratio def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings=None ) -> Grid2DIrregular: """ Returns an image mesh by running the balanced Hilbert curve on the weight map. diff --git a/autoarray/inversion/pixelization/image_mesh/kmeans.py b/autoarray/inversion/pixelization/image_mesh/kmeans.py index 4f051d035..8a7a25c80 100644 --- a/autoarray/inversion/pixelization/image_mesh/kmeans.py +++ b/autoarray/inversion/pixelization/image_mesh/kmeans.py @@ -51,7 +51,7 @@ def __init__( ) def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings=None ) -> Grid2DIrregular: """ Returns an image mesh by running a KMeans clustering algorithm on the weight map. diff --git a/autoarray/inversion/pixelization/image_mesh/overlay.py b/autoarray/inversion/pixelization/image_mesh/overlay.py index e616cf904..2f3591631 100644 --- a/autoarray/inversion/pixelization/image_mesh/overlay.py +++ b/autoarray/inversion/pixelization/image_mesh/overlay.py @@ -185,7 +185,7 @@ def __init__(self, shape=(3, 3)): self.shape = (int(shape[0]), int(shape[1])) def image_plane_mesh_grid_from( - self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings = None + self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings=None ) -> Grid2DIrregular: """ Returns an image-mesh by overlaying a uniform grid of (y,x) coordinates over the masked image that the diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py index 15ee1e415..d44c90e44 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_abstract.py @@ -4,7 +4,8 @@ import autoarray as aa -def test__mesh_pixels_per_image_pixels_from(): +@pytest.fixture(name="grid") +def make_grid(): mask = aa.Mask2D.circular( shape_native=(3, 3), radius=2.0, @@ -12,14 +13,29 @@ def test__mesh_pixels_per_image_pixels_from(): sub_size=1, ) - grid = aa.Grid2D.from_mask(mask=mask) + return aa.Grid2D.from_mask(mask=mask) - mesh_grid = aa.Grid2DIrregular( - values=[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 1.0), (0.0, 1.0), (-1.0, -1.0)] + +@pytest.fixture(name="mesh_grid") +def make_mesh_grid(): + return aa.Grid2DIrregular( + values=[ + (0.0, 0.0), + (0.0, 0.0), + (0.0, 0.0), + (0.0, 1.0), + (0.0, 1.0), + (-1.0, -1.0), + ] ) - image_mesh = aa.image_mesh.Hilbert(pixels=8) +@pytest.fixture(name="image_mesh") +def make_image_mesh(): + return aa.image_mesh.Hilbert(pixels=8) + + +def test__mesh_pixels_per_image_pixels_from(grid, mesh_grid, image_mesh): mesh_pixels_per_image_pixels = image_mesh.mesh_pixels_per_image_pixels_from( grid=grid, mesh_grid=mesh_grid ) @@ -28,45 +44,66 @@ def test__mesh_pixels_per_image_pixels_from(): np.array([[0, 0, 0], [0, 3, 2], [1, 0, 0]]), 1.0e-4 ) -def test__check_mesh_pixels_per_image_pixels(): - - mask = aa.Mask2D.circular( - shape_native=(3, 3), - radius=2.0, - pixel_scales=1.0, - sub_size=1, - ) - - grid = aa.Grid2D.from_mask(mask=mask) - - mesh_grid = aa.Grid2DIrregular( - values=[(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 1.0), (0.0, 1.0), (-1.0, -1.0)] - ) - - image_mesh = aa.image_mesh.Hilbert(pixels=8) +def test__check_mesh_pixels_per_image_pixels(grid, mesh_grid, image_mesh): image_mesh.check_mesh_pixels_per_image_pixels( - grid=grid, - mesh_grid=mesh_grid, - settings=None + grid=grid, mesh_grid=mesh_grid, settings=None ) image_mesh.check_mesh_pixels_per_image_pixels( grid=grid, mesh_grid=mesh_grid, - settings=aa.SettingsInversion(image_mesh_min_mesh_pixels_per_pixel=3, image_mesh_min_mesh_number=1) + settings=aa.SettingsInversion( + image_mesh_min_mesh_pixels_per_pixel=3, image_mesh_min_mesh_number=1 + ), ) with pytest.raises(aa.exc.InversionException): image_mesh.check_mesh_pixels_per_image_pixels( grid=grid, mesh_grid=mesh_grid, - settings=aa.SettingsInversion(image_mesh_min_mesh_pixels_per_pixel=5, image_mesh_min_mesh_number=1) + settings=aa.SettingsInversion( + image_mesh_min_mesh_pixels_per_pixel=5, image_mesh_min_mesh_number=1 + ), ) with pytest.raises(aa.exc.InversionException): image_mesh.check_mesh_pixels_per_image_pixels( grid=grid, mesh_grid=mesh_grid, - settings=aa.SettingsInversion(image_mesh_min_mesh_pixels_per_pixel=3, image_mesh_min_mesh_number=2) - ) \ No newline at end of file + settings=aa.SettingsInversion( + image_mesh_min_mesh_pixels_per_pixel=3, image_mesh_min_mesh_number=2 + ), + ) + + +def test__check_adapt_background_pixels(grid, mesh_grid, image_mesh): + adapt_data = aa.Array2D.no_mask( + values=[[0.05, 0.05, 0.05], [0.05, 0.6, 0.05], [0.05, 0.05, 0.05]], + pixel_scales=(1.0, 1.0), + ) + + image_mesh.check_adapt_background_pixels( + grid=grid, mesh_grid=mesh_grid, adapt_data=adapt_data, settings=None + ) + + image_mesh.check_adapt_background_pixels( + grid=grid, + mesh_grid=mesh_grid, + adapt_data=adapt_data, + settings=aa.SettingsInversion( + image_mesh_adapt_background_percent_threshold=0.05, + image_mesh_adapt_background_percent_check=0.9, + ), + ) + + with pytest.raises(aa.exc.InversionException): + image_mesh.check_adapt_background_pixels( + grid=grid, + mesh_grid=mesh_grid, + adapt_data=adapt_data, + settings=aa.SettingsInversion( + image_mesh_adapt_background_percent_threshold=0.8, + image_mesh_adapt_background_percent_check=0.5, + ), + ) diff --git a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py index 8b5f44acc..3286f4a2e 100644 --- a/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py +++ b/test_autoarray/inversion/pixelization/image_mesh/test_hilbert.py @@ -25,4 +25,3 @@ def test__image_plane_mesh_grid_from(): [-1.02590674, -1.70984456], 1.0e-4, ) -