From 7f869aa41b992c0e81fa6e8681cc5998a4e1dcb2 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 18:49:23 +0000 Subject: [PATCH 01/17] mask circular converts and some aspect simplified --- autoarray/geometry/geometry_util.py | 26 ++-- .../pixelization/border_relocator.py | 2 +- .../inversion/pixelization/mesh/mesh_util.py | 2 +- autoarray/mask/abstract_mask.py | 1 + autoarray/mask/mask_2d_util.py | 114 ++++++------------ autoarray/operators/contour.py | 4 +- .../over_sampling/over_sample_util.py | 4 +- .../operators/over_sampling/over_sampler.py | 1 + autoarray/plot/multi_plotters.py | 2 +- autoarray/structures/arrays/array_2d_util.py | 40 +++--- autoarray/structures/grids/grid_2d_util.py | 8 +- autoarray/structures/vectors/uniform.py | 5 +- test_autoarray/mask/test_mask_2d_util.py | 8 -- 13 files changed, 89 insertions(+), 128 deletions(-) diff --git a/autoarray/geometry/geometry_util.py b/autoarray/geometry/geometry_util.py index bbe7bc601..a795d42ee 100644 --- a/autoarray/geometry/geometry_util.py +++ b/autoarray/geometry/geometry_util.py @@ -182,7 +182,7 @@ def convert_pixel_scales_2d(pixel_scales: ty.PixelScales) -> Tuple[float, float] @numba_util.jit() def central_pixel_coordinates_2d_from( - shape_native: Tuple[int, int] + shape_native: Tuple[int, int], ) -> Tuple[float, float]: """ Returns the central pixel coordinates of a 2D geometry (and therefore a 2D data structure like an ``Array2D``) @@ -477,7 +477,6 @@ def grid_pixels_2d_slim_from( pixel_scales=(0.5, 0.5), origin=(0.0, 0.0)) """ - centres_scaled = central_scaled_coordinate_2d_from( shape_native=shape_native, pixel_scales=pixel_scales, origin=origin ) @@ -544,7 +543,6 @@ def grid_pixel_centres_2d_slim_from( pixel_scales=(0.5, 0.5), origin=(0.0, 0.0)) """ - centres_scaled = central_scaled_coordinate_2d_from( shape_native=shape_native, pixel_scales=pixel_scales, origin=origin ) @@ -629,8 +627,10 @@ def grid_pixel_indexes_2d_slim_from( if use_jax: grid_pixel_indexes_2d_slim = ( - grid_pixels_2d_slim * np.array([shape_native[1], 1]) - ).sum(axis=1).astype(int) + (grid_pixels_2d_slim * np.array([shape_native[1], 1])) + .sum(axis=1) + .astype(int) + ) else: grid_pixel_indexes_2d_slim = np.zeros(grid_pixels_2d_slim.shape[0]) @@ -690,7 +690,9 @@ def grid_scaled_2d_slim_from( centres_scaled = np.array(centres_scaled) pixel_scales = np.array(pixel_scales) sign = np.array([-1, 1]) - grid_scaled_2d_slim = (grid_pixels_2d_slim - centres_scaled - 0.5) * pixel_scales * sign + grid_scaled_2d_slim = ( + (grid_pixels_2d_slim - centres_scaled - 0.5) * pixel_scales * sign + ) else: grid_scaled_2d_slim = np.zeros((grid_pixels_2d_slim.shape[0], 2)) @@ -755,7 +757,7 @@ def grid_pixel_centres_2d_from( centres_scaled = np.array(centres_scaled) pixel_scales = np.array(pixel_scales) sign = np.array([-1.0, 1.0]) - grid_pixels_2d = ( + grid_pixels_2d = ( (sign * grid_scaled_2d / pixel_scales) + centres_scaled + 0.5 ).astype(int) else: @@ -764,17 +766,21 @@ def grid_pixel_centres_2d_from( for y in range(grid_scaled_2d.shape[0]): for x in range(grid_scaled_2d.shape[1]): grid_pixels_2d[y, x, 0] = int( - (-grid_scaled_2d[y, x, 0] / pixel_scales[0]) + centres_scaled[0] + 0.5 + (-grid_scaled_2d[y, x, 0] / pixel_scales[0]) + + centres_scaled[0] + + 0.5 ) grid_pixels_2d[y, x, 1] = int( - (grid_scaled_2d[y, x, 1] / pixel_scales[1]) + centres_scaled[1] + 0.5 + (grid_scaled_2d[y, x, 1] / pixel_scales[1]) + + centres_scaled[1] + + 0.5 ) return grid_pixels_2d def extent_symmetric_from( - extent: Tuple[float, float, float, float] + extent: Tuple[float, float, float, float], ) -> Tuple[float, float, float, float]: """ Given an input extent of the form (x_min, x_max, y_min, y_max), this function returns an extent which is diff --git a/autoarray/inversion/pixelization/border_relocator.py b/autoarray/inversion/pixelization/border_relocator.py index 73a8d0d28..737444b53 100644 --- a/autoarray/inversion/pixelization/border_relocator.py +++ b/autoarray/inversion/pixelization/border_relocator.py @@ -48,7 +48,7 @@ def sub_slim_indexes_for_slim_index_via_mask_2d_from( sub_mask_1d_indexes_for_mask_1d_index = sub_mask_1d_indexes_for_mask_1d_index_from(mask=mask, sub_size=2) """ - total_pixels = mask_2d_util.total_pixels_2d_from(mask_2d=mask_2d) + total_pixels = np.sum(~mask_2d) sub_slim_indexes_for_slim_index = [[] for _ in range(total_pixels)] diff --git a/autoarray/inversion/pixelization/mesh/mesh_util.py b/autoarray/inversion/pixelization/mesh/mesh_util.py index 78cb4a860..305b56b72 100644 --- a/autoarray/inversion/pixelization/mesh/mesh_util.py +++ b/autoarray/inversion/pixelization/mesh/mesh_util.py @@ -7,7 +7,7 @@ @numba_util.jit() def rectangular_neighbors_from( - shape_native: Tuple[int, int] + shape_native: Tuple[int, int], ) -> Tuple[np.ndarray, np.ndarray]: """ Returns the 4 (or less) adjacent neighbors of every pixel on a rectangular pixelization as an ndarray of shape diff --git a/autoarray/mask/abstract_mask.py b/autoarray/mask/abstract_mask.py index bc80d6a1b..5ffc0bf7f 100644 --- a/autoarray/mask/abstract_mask.py +++ b/autoarray/mask/abstract_mask.py @@ -4,6 +4,7 @@ import logging from autoarray.numpy_wrapper import np, use_jax + if use_jax: import jax from pathlib import Path diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index f79f7acff..6a628a5dc 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -8,118 +8,78 @@ from autoarray.numpy_wrapper import use_jax, np as jnp -@numba_util.jit() def mask_2d_centres_from( - shape_native: Tuple[int, int], - pixel_scales: ty.PixelScales, - centre: Tuple[float, float], -) -> Tuple[float, float]: + shape_native: tuple[int, int], + pixel_scales: tuple[float, float], + centre: tuple[float, float], +) -> tuple[float, float]: """ - Returns the (y,x) scaled central coordinates of a mask from its shape, pixel-scales and centre. + Compute the (y, x) scaled central coordinates of a mask given its shape, pixel-scales, and centre. - The coordinate system is defined such that the positive y axis is up and positive x axis is right. + The coordinate system is defined such that the positive y-axis is up and the positive x-axis is right. Parameters ---------- shape_native - The (y,x) shape of the 2D array the scaled centre is computed for. + The shape of the 2D array in pixels. pixel_scales - The (y,x) scaled units to pixel units conversion factor of the 2D array. - centre : (float, flloat) - The (y,x) centre of the 2D mask. - - Returns - ------- - tuple (float, float) - The (y,x) scaled central coordinates of the input array. - - Examples - -------- - centres_scaled = centres_from(shape=(5,5), pixel_scales=(0.5, 0.5), centre=(0.0, 0.0)) - """ - y_centre_scaled = (float(shape_native[0] - 1) / 2) - (centre[0] / pixel_scales[0]) - x_centre_scaled = (float(shape_native[1] - 1) / 2) + (centre[1] / pixel_scales[1]) - - return (y_centre_scaled, x_centre_scaled) - - -@numba_util.jit() -def total_pixels_2d_from(mask_2d: np.ndarray) -> int: - """ - Returns the total number of unmasked pixels in a mask. - - Parameters - ---------- - mask_2d - A 2D array of bools, where `False` values are unmasked and included when counting pixels. + The conversion factors from pixels to scaled units. + centre + The central coordinate of the mask in scaled units. Returns ------- - int - The total number of pixels that are unmasked. + The (y, x) scaled central coordinates of the input array. Examples -------- - - mask = np.array([[True, False, True], - [False, False, False] - [True, False, True]]) - - total_regular_pixels = total_regular_pixels_from(mask=mask) + centres_scaled = mask_2d_centres_from(shape_native=(5, 5), pixel_scales=(0.5, 0.5), centre=(0.0, 0.0)) """ - if use_jax: - return (~mask_2d.astype(bool)).sum() - - else: - total_regular_pixels = 0 - - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - if not mask_2d[y, x]: - total_regular_pixels += 1 - - return total_regular_pixels + return ( + 0.5 * (shape_native[0] - 1) - (centre[0] / pixel_scales[0]), + 0.5 * (shape_native[1] - 1) + (centre[1] / pixel_scales[1]), + ) -@numba_util.jit() def mask_2d_circular_from( - shape_native: Tuple[int, int], - pixel_scales: ty.PixelScales, + shape_native: tuple[int, int], + pixel_scales: tuple[float, float], radius: float, - centre: Tuple[float, float] = (0.0, 0.0), + centre: tuple[float, float] = (0.0, 0.0), ) -> np.ndarray: """ - Returns a circular mask from the 2D mask array shape and radius of the circle. + Create a circular mask within a 2D array. - This creates a 2D array where all values within the mask radius are unmasked and therefore `False`. + This generates a 2D array where all values within the specified radius are unmasked (set to `False`). Parameters ---------- - shape_native: Tuple[int, int] - The (y,x) shape of the mask in units of pixels. + shape_native + The shape of the mask array in pixels. pixel_scales - The scaled units to pixel units conversion factor of each pixel. + The conversion factors from pixels to scaled units. radius - The radius (in scaled units) of the circle within which pixels unmasked. + The radius of the circular mask in scaled units. centre - The centre of the circle used to mask pixels. + The central coordinate of the circle in scaled units. Returns ------- - ndarray - The 2D mask array whose central pixels are masked as a circle. + The 2D mask array with the central region defined by the radius unmasked (False). Examples -------- - mask = mask_circular_from( - shape=(10, 10), pixel_scales=0.1, radius=0.5, centre=(0.0, 0.0)) + mask = mask_2d_circular_from(shape_native=(10, 10), pixel_scales=(0.1, 0.1), radius=0.5, centre=(0.0, 0.0)) """ centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) - ys, xs = np.indices(shape_native) - return (radius * radius) < ( - np.square((ys - centres_scaled[0]) * pixel_scales[0]) + - np.square((xs - centres_scaled[1]) * pixel_scales[1]) - ) + + y, x = np.ogrid[: shape_native[0], : shape_native[1]] + y_scaled = (y - centres_scaled[0]) * pixel_scales[0] + x_scaled = (x - centres_scaled[1]) * pixel_scales[1] + + distances_squared = x_scaled**2 + y_scaled**2 + + return distances_squared >= radius**2 @numba_util.jit() @@ -1047,7 +1007,7 @@ def native_index_for_slim_index_2d_from( if use_jax: return jnp.stack(jnp.nonzero(~mask_2d.astype(bool))).T else: - total_pixels = total_pixels_2d_from(mask_2d=mask_2d) + total_pixels = np.sum(~mask_2d) native_index_for_slim_index_2d = np.zeros(shape=(total_pixels, 2)) slim_index = 0 diff --git a/autoarray/operators/contour.py b/autoarray/operators/contour.py index a85a73105..c7da5c7f1 100644 --- a/autoarray/operators/contour.py +++ b/autoarray/operators/contour.py @@ -58,7 +58,9 @@ def contour_array(self): @property def contour_list(self): # make sure to use base numpy to convert JAX array back to a normal array - contour_indices_list = measure.find_contours(numpy.array(self.contour_array.array), 0) + contour_indices_list = measure.find_contours( + numpy.array(self.contour_array.array), 0 + ) if len(contour_indices_list) == 0: return [] diff --git a/autoarray/operators/over_sampling/over_sample_util.py b/autoarray/operators/over_sampling/over_sample_util.py index b0df135a8..a98276896 100644 --- a/autoarray/operators/over_sampling/over_sample_util.py +++ b/autoarray/operators/over_sampling/over_sample_util.py @@ -528,9 +528,7 @@ def binned_array_2d_from( grid_slim = grid_2d_slim_over_sampled_via_mask_from(mask=mask, pixel_scales=(0.5, 0.5), sub_size=1, origin=(0.0, 0.0)) """ - total_pixels = mask_2d_util.total_pixels_2d_from( - mask_2d=mask_2d, - ) + total_pixels = np.sum(~mask_2d) sub_fraction = 1.0 / sub_size**2 diff --git a/autoarray/operators/over_sampling/over_sampler.py b/autoarray/operators/over_sampling/over_sampler.py index d7187a33d..6492c00b7 100644 --- a/autoarray/operators/over_sampling/over_sampler.py +++ b/autoarray/operators/over_sampling/over_sampler.py @@ -11,6 +11,7 @@ from autofit.jax_wrapper import register_pytree_node_class + @register_pytree_node_class class OverSampler: def __init__(self, mask: Mask2D, sub_size: Union[int, Array2D]): diff --git a/autoarray/plot/multi_plotters.py b/autoarray/plot/multi_plotters.py index 5c2c5071e..a58d08c02 100644 --- a/autoarray/plot/multi_plotters.py +++ b/autoarray/plot/multi_plotters.py @@ -315,7 +315,7 @@ def output_to_fits( output_path = self.plotter_list[0].mat_plot_2d.output.output_path_from( format="fits_multi" ) - output_fits_file = Path(output_path)/ f"{filename}.fits" + output_fits_file = Path(output_path) / f"{filename}.fits" if remove_fits_first: output_fits_file.unlink(missing_ok=True) diff --git a/autoarray/structures/arrays/array_2d_util.py b/autoarray/structures/arrays/array_2d_util.py index f147baf41..b75dd859e 100644 --- a/autoarray/structures/arrays/array_2d_util.py +++ b/autoarray/structures/arrays/array_2d_util.py @@ -1,5 +1,6 @@ from __future__ import annotations from astropy.io import fits + # import numpy as np import os from pathlib import Path @@ -15,6 +16,7 @@ from autoarray import exc from autoarray.numpy_wrapper import use_jax, np, jit from functools import partial + if use_jax: import jax @@ -30,10 +32,7 @@ def convert_array(array: Union[np.ndarray, List]) -> np.ndarray: """ if use_jax: array = jax.lax.cond( - type(array) is list, - lambda _: np.asarray(array), - lambda _: array, - None + type(array) is list, lambda _: np.asarray(array), lambda _: array, None ) elif type(array) is list: array = np.asarray(array) @@ -46,13 +45,11 @@ def exception_message(): raise exc.ArrayException( "An array input into the Array2D.__new__ method is not of shape 1." ) + cond = len(array_2d.shape) != 1 if use_jax: jax.lax.cond( - cond, - lambda _: jax.debug.callback(exception_message), - lambda _: None, - None + cond, lambda _: jax.debug.callback(exception_message), lambda _: None, None ) elif cond: exception_message() @@ -74,6 +71,7 @@ def check_array_2d_and_mask_2d(array_2d: np.ndarray, mask_2d: Mask2D): mask_2d The mask of the output Array2D. """ + def exception_message_1(): raise exc.ArrayException( f""" @@ -90,14 +88,17 @@ def exception_message_1(): Input mask_2d.shape_native = {mask_2d.shape_native} """ ) - cond_1 = (len(array_2d.shape) == 1) and (array_2d.shape[0] != mask_2d.pixels_in_mask) + + cond_1 = (len(array_2d.shape) == 1) and ( + array_2d.shape[0] != mask_2d.pixels_in_mask + ) if use_jax: jax.lax.cond( cond_1, lambda _: jax.debug.callback(exception_message_1), lambda _: None, - None + None, ) elif cond_1: exception_message_1() @@ -115,6 +116,7 @@ def exception_message_2(): Input mask_2d shape_native = {mask_2d.shape_native} """ ) + cond_2 = (len(array_2d.shape) == 2) and (array_2d.shape != mask_2d.shape_native) if use_jax: @@ -122,7 +124,7 @@ def exception_message_2(): cond_2, lambda _: jax.debug.callback(exception_message_2), lambda _: None, - None + None, ) elif cond_2: exception_message_2() @@ -578,9 +580,7 @@ def array_2d_slim_from( if use_jax: array_2d_slim = array_2d_native[~mask_2d.astype(bool)] else: - total_pixels = mask_2d_util.total_pixels_2d_from( - mask_2d=mask_2d, - ) + total_pixels = np.sum(~mask_2d) array_2d_slim = np.zeros(shape=total_pixels) index = 0 @@ -681,9 +681,11 @@ def array_2d_via_indexes_from( The native 2D array of values mapped from the slimmed array with dimensions (total_values, total_values). """ if use_jax: - array_native_2d = np.zeros(shape).at[ - tuple(native_index_for_slim_index_2d.T) - ].set(array_2d_slim) + array_native_2d = ( + np.zeros(shape) + .at[tuple(native_index_for_slim_index_2d.T)] + .set(array_2d_slim) + ) else: array_native_2d = np.zeros(shape) @@ -728,9 +730,7 @@ def array_2d_slim_complex_from( A 1D array of values mapped from the 2D array with dimensions (total_unmasked_pixels). """ - total_pixels = mask_2d_util.total_pixels_2d_from( - mask_2d=mask, - ) + total_pixels = np.sum(~mask_2d) array_1d = 0 + 0j * np.zeros(shape=total_pixels) index = 0 diff --git a/autoarray/structures/grids/grid_2d_util.py b/autoarray/structures/grids/grid_2d_util.py index c659a032b..44c75c7c5 100644 --- a/autoarray/structures/grids/grid_2d_util.py +++ b/autoarray/structures/grids/grid_2d_util.py @@ -273,7 +273,7 @@ def grid_2d_slim_via_mask_from( grid_slim = grid_2d_slim_via_mask_from(mask=mask, pixel_scales=(0.5, 0.5), origin=(0.0, 0.0)) """ - total_pixels = mask_2d_util.total_pixels_2d_from(mask_2d) + total_pixels = np.sum(~mask_2d) centres_scaled = geometry_util.central_scaled_coordinate_2d_from( shape_native=mask_2d.shape, pixel_scales=pixel_scales, origin=origin @@ -284,8 +284,10 @@ def grid_2d_slim_via_mask_from( pixel_scales = np.array(pixel_scales) sign = np.array([-1.0, 1.0]) grid_slim = ( - np.stack(np.nonzero(~mask_2d.astype(bool))).T - centres_scaled - ) * sign * pixel_scales + (np.stack(np.nonzero(~mask_2d.astype(bool))).T - centres_scaled) + * sign + * pixel_scales + ) else: index = 0 grid_slim = np.zeros(shape=(total_pixels, 2)) diff --git a/autoarray/structures/vectors/uniform.py b/autoarray/structures/vectors/uniform.py index 0a882926c..89d589139 100644 --- a/autoarray/structures/vectors/uniform.py +++ b/autoarray/structures/vectors/uniform.py @@ -1,4 +1,5 @@ import logging + # import numpy as np from autofit.jax_wrapper import numpy as np, use_jax from typing import List, Optional, Tuple, Union @@ -399,9 +400,7 @@ def magnitudes(self) -> Array2D: s = self.array else: s = self - return Array2D( - values=np.sqrt(s[:, 0] ** 2.0 + s[:, 1] ** 2.0), mask=self.mask - ) + return Array2D(values=np.sqrt(s[:, 0] ** 2.0 + s[:, 1] ** 2.0), mask=self.mask) @property def y(self) -> Array2D: diff --git a/test_autoarray/mask/test_mask_2d_util.py b/test_autoarray/mask/test_mask_2d_util.py index a3b05c894..ef2d3481a 100644 --- a/test_autoarray/mask/test_mask_2d_util.py +++ b/test_autoarray/mask/test_mask_2d_util.py @@ -5,14 +5,6 @@ import pytest -def test__total_pixels_2d_from(): - mask_2d = np.array( - [[True, False, True], [False, False, False], [True, False, True]] - ) - - assert util.mask_2d.total_pixels_2d_from(mask_2d=mask_2d) == 5 - - def test__total_edge_pixels_from_mask(): mask_2d = np.array( [ From 581aa75e76a752138c9b560fdf99660af8cbb878 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 18:58:59 +0000 Subject: [PATCH 02/17] mask_circular_annular_from converted --- autoarray/mask/mask_2d_util.py | 50 ++++++++++++++-------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 6a628a5dc..3269f060f 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -82,60 +82,50 @@ def mask_2d_circular_from( return distances_squared >= radius**2 -@numba_util.jit() def mask_2d_circular_annular_from( - shape_native: Tuple[int, int], - pixel_scales: ty.PixelScales, + shape_native: tuple[int, int], + pixel_scales: tuple[float, float], inner_radius: float, outer_radius: float, - centre: Tuple[float, float] = (0.0, 0.0), + centre: tuple[float, float] = (0.0, 0.0), ) -> np.ndarray: """ - Returns an circular annular mask from an input inner and outer mask radius and shape. + Create a circular annular mask within a 2D array. - This creates a 2D array where all values within the inner and outer radii are unmasked and therefore `False`. + This generates a 2D array where all values within the specified inner and outer radii are unmasked (set to `False`). Parameters ---------- shape_native - The (y,x) shape of the mask in units of pixels. + The shape of the mask array in pixels. pixel_scales - The scaled units to pixel units conversion factor of each pixel. + The conversion factors from pixels to scaled units. inner_radius - The radius (in scaled units) of the inner circle outside of which pixels are unmasked. + The inner radius of the annular mask in scaled units. outer_radius - The radius (in scaled units) of the outer circle within which pixels are unmasked. + The outer radius of the annular mask in scaled units. centre - The centre of the annulus used to mask pixels. + The central coordinate of the annulus in scaled units. Returns ------- - ndarray - The 2D mask array whose central pixels are masked as a annulus. + The 2D mask array with the region between the inner and outer radii unmasked (False). Examples -------- - mask = mask_annnular_from( - shape=(10, 10), pixel_scales=0.1, inner_radius=0.5, outer_radius=1.5, centre=(0.0, 0.0)) - """ - - mask_2d = np.full(shape_native, True) - - centres_scaled = mask_2d_centres_from( - shape_native=mask_2d.shape, pixel_scales=pixel_scales, centre=centre + mask = mask_2d_circular_annular_from( + shape_native=(10, 10), pixel_scales=(0.1, 0.1), inner_radius=0.5, outer_radius=1.5, centre=(0.0, 0.0) ) + """ + centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - y_scaled = (y - centres_scaled[0]) * pixel_scales[0] - x_scaled = (x - centres_scaled[1]) * pixel_scales[1] - - r_scaled = np.sqrt(x_scaled**2 + y_scaled**2) + y, x = np.ogrid[:shape_native[0], :shape_native[1]] + y_scaled = (y - centres_scaled[0]) * pixel_scales[0] + x_scaled = (x - centres_scaled[1]) * pixel_scales[1] - if outer_radius >= r_scaled >= inner_radius: - mask_2d[y, x] = False + distances_squared = x_scaled**2 + y_scaled**2 - return mask_2d + return ~((distances_squared >= inner_radius**2) & (distances_squared <= outer_radius**2)) @numba_util.jit() From ed60fdac85df548baa8eca0ec36e817188610a64 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 19:00:04 +0000 Subject: [PATCH 03/17] update typing --- autoarray/mask/mask_2d_util.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 3269f060f..102ff771b 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -9,10 +9,10 @@ def mask_2d_centres_from( - shape_native: tuple[int, int], - pixel_scales: tuple[float, float], - centre: tuple[float, float], -) -> tuple[float, float]: + shape_native: Tuple[int, int], + pixel_scales: Tuple[float, float], + centre: Tuple[float, float], +) -> Tuple[float, float]: """ Compute the (y, x) scaled central coordinates of a mask given its shape, pixel-scales, and centre. @@ -42,10 +42,10 @@ def mask_2d_centres_from( def mask_2d_circular_from( - shape_native: tuple[int, int], - pixel_scales: tuple[float, float], + shape_native: Tuple[int, int], + pixel_scales: Tuple[float, float], radius: float, - centre: tuple[float, float] = (0.0, 0.0), + centre: Tuple[float, float] = (0.0, 0.0), ) -> np.ndarray: """ Create a circular mask within a 2D array. @@ -83,11 +83,11 @@ def mask_2d_circular_from( def mask_2d_circular_annular_from( - shape_native: tuple[int, int], - pixel_scales: tuple[float, float], + shape_native: Tuple[int, int], + pixel_scales: Tuple[float, float], inner_radius: float, outer_radius: float, - centre: tuple[float, float] = (0.0, 0.0), + centre: Tuple[float, float] = (0.0, 0.0), ) -> np.ndarray: """ Create a circular annular mask within a 2D array. From 14609842c65eb7713912a518e2042e440a54840c Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 19:01:52 +0000 Subject: [PATCH 04/17] remove anti annular --- autoarray/mask/mask_2d.py | 59 --------------- autoarray/mask/mask_2d_util.py | 65 ----------------- test_autoarray/mask/test_mask_2d.py | 39 ---------- test_autoarray/mask/test_mask_2d_util.py | 91 ------------------------ 4 files changed, 254 deletions(-) diff --git a/autoarray/mask/mask_2d.py b/autoarray/mask/mask_2d.py index 18a4c8fea..9cecf4b24 100644 --- a/autoarray/mask/mask_2d.py +++ b/autoarray/mask/mask_2d.py @@ -380,65 +380,6 @@ def circular_annular( invert=invert, ) - @classmethod - def circular_anti_annular( - cls, - shape_native: Tuple[int, int], - inner_radius: float, - outer_radius: float, - outer_radius_2: float, - pixel_scales: ty.PixelScales, - origin: Tuple[float, float] = (0.0, 0.0), - centre: Tuple[float, float] = (0.0, 0.0), - invert: bool = False, - ) -> "Mask2D": - """ - Returns a Mask2D (see *Mask2D.__new__*) where all `False` entries are within an inner circle and second - outer circle, forming an inverse annulus. - - The `inner_radius`, `outer_radius`, `outer_radius_2` and `centre` are all input in scaled units. - - Parameters - ---------- - shape_native - The (y,x) shape of the mask in units of pixels. - inner_radius - The inner radius in scaled units of the annulus within which pixels are `False` and unmasked. - outer_radius - The first outer radius in scaled units of the annulus within which pixels are `True` and masked. - outer_radius_2 - The second outer radius in scaled units of the annulus within which pixels are `False` and unmasked and - outside of which all entries are `True` and masked. - 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) scaled units origin of the mask's coordinate system. - centre - The (y,x) scaled units centre of the anti-annulus used to mask pixels. - invert - If `True`, the `bool`'s of the input `mask` are inverted, for example `False`'s become `True` - and visa versa. - """ - - pixel_scales = geometry_util.convert_pixel_scales_2d(pixel_scales=pixel_scales) - - mask = mask_2d_util.mask_2d_circular_anti_annular_from( - shape_native=shape_native, - pixel_scales=pixel_scales, - inner_radius=inner_radius, - outer_radius=outer_radius, - outer_radius_2_scaled=outer_radius_2, - centre=centre, - ) - - return cls( - mask=mask, - pixel_scales=pixel_scales, - origin=origin, - invert=invert, - ) - @classmethod def elliptical( cls, diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 102ff771b..c8c0bf62f 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -128,71 +128,6 @@ def mask_2d_circular_annular_from( return ~((distances_squared >= inner_radius**2) & (distances_squared <= outer_radius**2)) -@numba_util.jit() -def mask_2d_circular_anti_annular_from( - shape_native: Tuple[int, int], - pixel_scales: ty.PixelScales, - inner_radius: float, - outer_radius: float, - outer_radius_2_scaled: float, - centre: Tuple[float, float] = (0.0, 0.0), -) -> np.ndarray: - """ - Returns an anti-annular mask from an input inner and outer mask radius and shape. The anti-annular is analogous to - the annular mask but inverted, whereby its unmasked values are those inside the annulus. - - This creates a 2D array where all values outside the inner and outer radii are unmasked and therefore `False`. - - Parameters - ---------- - shape_native - The (y,x) shape of the mask in units of pixels. - pixel_scales - The scaled units to pixel units conversion factor of each pixel. - inner_radius - The inner radius in scaled units of the annulus within which pixels are `False` and unmasked. - outer_radius - The first outer radius in scaled units of the annulus within which pixels are `True` and masked. - outer_radius_2 - The second outer radius in scaled units of the annulus within which pixels are `False` and unmasked and - outside of which all entries are `True` and masked. - centre - The centre of the annulus used to mask pixels. - - Returns - ------- - ndarray - The 2D mask array whose central pixels are masked as a annulus. - - Examples - -------- - mask = mask_annnular_from( - shape=(10, 10), pixel_scales=0.1, inner_radius=0.5, outer_radius=1.5, centre=(0.0, 0.0)) - - """ - - mask_2d = np.full(shape_native, True) - - centres_scaled = mask_2d_centres_from( - shape_native=mask_2d.shape, pixel_scales=pixel_scales, centre=centre - ) - - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - y_scaled = (y - centres_scaled[0]) * pixel_scales[0] - x_scaled = (x - centres_scaled[1]) * pixel_scales[1] - - r_scaled = np.sqrt(x_scaled**2 + y_scaled**2) - - if ( - inner_radius >= r_scaled - or outer_radius_2_scaled >= r_scaled >= outer_radius - ): - mask_2d[y, x] = False - - return mask_2d - - def mask_2d_via_pixel_coordinates_from( shape_native: Tuple[int, int], pixel_coordinates: [list], buffer: int = 0 ) -> np.ndarray: diff --git a/test_autoarray/mask/test_mask_2d.py b/test_autoarray/mask/test_mask_2d.py index 3b80030e6..2ad05a0a7 100644 --- a/test_autoarray/mask/test_mask_2d.py +++ b/test_autoarray/mask/test_mask_2d.py @@ -141,45 +141,6 @@ def test__circular_annular(): assert mask.origin == (0.0, 0.0) assert mask.mask_centre == (0.0, 0.0) - -def test__circular_anti_annular(): - mask_via_util = aa.util.mask_2d.mask_2d_circular_anti_annular_from( - shape_native=(9, 9), - pixel_scales=(1.2, 1.2), - inner_radius=0.8, - outer_radius=2.2, - outer_radius_2_scaled=3.0, - centre=(0.0, 0.0), - ) - - mask = aa.Mask2D.circular_anti_annular( - shape_native=(9, 9), - pixel_scales=(1.2, 1.2), - inner_radius=0.8, - outer_radius=2.2, - outer_radius_2=3.0, - centre=(0.0, 0.0), - ) - - assert (mask == mask_via_util).all() - assert mask.origin == (0.0, 0.0) - assert mask.mask_centre == (0.0, 0.0) - - mask = aa.Mask2D.circular_anti_annular( - shape_native=(9, 9), - pixel_scales=(1.2, 1.2), - inner_radius=0.8, - outer_radius=2.2, - outer_radius_2=3.0, - centre=(0.0, 0.0), - invert=True, - ) - - assert (mask == np.invert(mask_via_util)).all() - assert mask.origin == (0.0, 0.0) - assert mask.mask_centre == (0.0, 0.0) - - def test__elliptical(): mask_via_util = aa.util.mask_2d.mask_2d_elliptical_from( shape_native=(8, 5), diff --git a/test_autoarray/mask/test_mask_2d_util.py b/test_autoarray/mask/test_mask_2d_util.py index ef2d3481a..344f8a1c1 100644 --- a/test_autoarray/mask/test_mask_2d_util.py +++ b/test_autoarray/mask/test_mask_2d_util.py @@ -234,97 +234,6 @@ def test__mask_2d_circular_annular_from__input_centre(): ).all() -def test__mask_2d_circular_anti_annular_from(): - mask = util.mask_2d.mask_2d_circular_anti_annular_from( - shape_native=(5, 5), - pixel_scales=(1.0, 1.0), - inner_radius=0.5, - outer_radius=10.0, - outer_radius_2_scaled=20.0, - ) - - assert ( - mask - == np.array( - [ - [True, True, True, True, True], - [True, True, True, True, True], - [True, True, False, True, True], - [True, True, True, True, True], - [True, True, True, True, True], - ] - ) - ).all() - - mask = util.mask_2d.mask_2d_circular_anti_annular_from( - shape_native=(5, 5), - pixel_scales=(0.1, 1.0), - inner_radius=1.5, - outer_radius=10.0, - outer_radius_2_scaled=20.0, - ) - - assert ( - mask - == np.array( - [ - [True, False, False, False, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, False, False, False, True], - ] - ) - ).all() - - mask = util.mask_2d.mask_2d_circular_anti_annular_from( - shape_native=(5, 5), - pixel_scales=(1.0, 1.0), - inner_radius=0.5, - outer_radius=1.5, - outer_radius_2_scaled=20.0, - ) - - assert ( - mask - == np.array( - [ - [False, False, False, False, False], - [False, True, True, True, False], - [False, True, False, True, False], - [False, True, True, True, False], - [False, False, False, False, False], - ] - ) - ).all() - - -def test__mask_2d_circular_anti_annular_from__include_centre(): - mask = util.mask_2d.mask_2d_circular_anti_annular_from( - shape_native=(7, 7), - pixel_scales=(3.0, 3.0), - inner_radius=1.5, - outer_radius=4.5, - outer_radius_2_scaled=8.7, - centre=(-3.0, 3.0), - ) - - assert ( - mask - == np.array( - [ - [True, True, True, True, True, True, True], - [True, True, True, True, True, True, True], - [True, True, False, False, False, False, False], - [True, True, False, True, True, True, False], - [True, True, False, True, False, True, False], - [True, True, False, True, True, True, False], - [True, True, False, False, False, False, False], - ] - ) - ).all() - - def test__mask_2d_elliptical_from(): mask = util.mask_2d.mask_2d_elliptical_from( shape_native=(3, 3), From c4fe49f2fef9f71afa55c0742ecfa053d95a1ed8 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 19:04:01 +0000 Subject: [PATCH 05/17] move from pixel coordinates --- autoarray/mask/mask_2d_util.py | 68 +++++++++++++++-------------- test_autoarray/mask/test_mask_2d.py | 1 + 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index c8c0bf62f..1f56b96b1 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -119,44 +119,15 @@ def mask_2d_circular_annular_from( """ centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) - y, x = np.ogrid[:shape_native[0], :shape_native[1]] + y, x = np.ogrid[: shape_native[0], : shape_native[1]] y_scaled = (y - centres_scaled[0]) * pixel_scales[0] x_scaled = (x - centres_scaled[1]) * pixel_scales[1] distances_squared = x_scaled**2 + y_scaled**2 - return ~((distances_squared >= inner_radius**2) & (distances_squared <= outer_radius**2)) - - -def mask_2d_via_pixel_coordinates_from( - shape_native: Tuple[int, int], pixel_coordinates: [list], buffer: int = 0 -) -> np.ndarray: - """ - Returns a mask where all unmasked `False` entries are defined from an input list of list of pixel coordinates. - - These may be buffed via an input ``buffer``, whereby all entries in all 8 neighboring directions by this - amount. - - Parameters - ---------- - shape_native (int, int) - The (y,x) shape of the mask in units of pixels. - pixel_coordinates : [[int, int]] - The input lists of 2D pixel coordinates where `False` entries are created. - buffer - All input ``pixel_coordinates`` are buffed with `False` entries in all 8 neighboring directions by this - amount. - """ - - mask_2d = np.full(shape=shape_native, fill_value=True) - - for y, x in pixel_coordinates: - mask_2d[y, x] = False - - if buffer == 0: - return mask_2d - else: - return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) + return ~( + (distances_squared >= inner_radius**2) & (distances_squared <= outer_radius**2) + ) @numba_util.jit() @@ -342,6 +313,37 @@ def mask_2d_elliptical_annular_from( return mask_2d +def mask_2d_via_pixel_coordinates_from( + shape_native: Tuple[int, int], pixel_coordinates: [list], buffer: int = 0 +) -> np.ndarray: + """ + Returns a mask where all unmasked `False` entries are defined from an input list of list of pixel coordinates. + + These may be buffed via an input ``buffer``, whereby all entries in all 8 neighboring directions by this + amount. + + Parameters + ---------- + shape_native (int, int) + The (y,x) shape of the mask in units of pixels. + pixel_coordinates : [[int, int]] + The input lists of 2D pixel coordinates where `False` entries are created. + buffer + All input ``pixel_coordinates`` are buffed with `False` entries in all 8 neighboring directions by this + amount. + """ + + mask_2d = np.full(shape=shape_native, fill_value=True) + + for y, x in pixel_coordinates: + mask_2d[y, x] = False + + if buffer == 0: + return mask_2d + else: + return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) + + @numba_util.jit() def blurring_mask_2d_from( mask_2d: np.ndarray, kernel_shape_native: Tuple[int, int] diff --git a/test_autoarray/mask/test_mask_2d.py b/test_autoarray/mask/test_mask_2d.py index 2ad05a0a7..eafac1efc 100644 --- a/test_autoarray/mask/test_mask_2d.py +++ b/test_autoarray/mask/test_mask_2d.py @@ -141,6 +141,7 @@ def test__circular_annular(): assert mask.origin == (0.0, 0.0) assert mask.mask_centre == (0.0, 0.0) + def test__elliptical(): mask_via_util = aa.util.mask_2d.mask_2d_elliptical_from( shape_native=(8, 5), From a5544b4ff9c93892a9bd98d466381fab420028ff Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 19:14:14 +0000 Subject: [PATCH 06/17] mask_2d_elliptical_From --- autoarray/mask/mask_2d_util.py | 60 ++++++++++++++++------------------ 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 1f56b96b1..a73e6c909 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -167,67 +167,65 @@ def elliptical_radius_from( return np.sqrt(x_scaled_elliptical**2.0 + (y_scaled_elliptical / axis_ratio) ** 2.0) -@numba_util.jit() + def mask_2d_elliptical_from( shape_native: Tuple[int, int], - pixel_scales: ty.PixelScales, + pixel_scales: Tuple[float, float], major_axis_radius: float, axis_ratio: float, angle: float, centre: Tuple[float, float] = (0.0, 0.0), ) -> np.ndarray: """ - Returns an elliptical mask from an input major-axis mask radius, axis-ratio, rotational angle, shape and - centre. + Create an elliptical mask within a 2D array. - This creates a 2D array where all values within the ellipse are unmasked and therefore `False`. + This generates a 2D array where all values within the specified ellipse are unmasked (set to `False`). Parameters ---------- - shape_native: Tuple[int, int] - The (y,x) shape of the mask in units of pixels. + shape_native + The shape of the mask array in pixels. pixel_scales - The scaled units to pixel units conversion factor of each pixel. + The conversion factors from pixels to scaled units. major_axis_radius - The major-axis (in scaled units) of the ellipse within which pixels are unmasked. + The major axis radius of the elliptical mask in scaled units. axis_ratio - The axis-ratio of the ellipse within which pixels are unmasked. + The axis ratio of the ellipse (minor axis / major axis). angle - The rotation angle of the ellipse within which pixels are unmasked, (counter-clockwise from the positive - x-axis). + The rotation angle of the ellipse in degrees, counter-clockwise from the positive x-axis. centre - The centre of the ellipse used to mask pixels. + The central coordinate of the ellipse in scaled units. Returns ------- - ndarray - The 2D mask array whose central pixels are masked as an ellipse. + np.ndarray + The 2D mask array with the elliptical region defined by the major axis radius unmasked (False). Examples -------- - mask = mask_elliptical_from( - shape=(10, 10), pixel_scales=0.1, major_axis_radius=0.5, ell_comps=(0.333333, 0.0), centre=(0.0, 0.0)) + mask = mask_2d_elliptical_from( + shape_native=(10, 10), pixel_scales=(0.1, 0.1), major_axis_radius=0.5, axis_ratio=0.5, angle=45.0, centre=(0.0, 0.0) + ) """ + centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) - mask_2d = np.full(shape_native, True) + y, x = np.ogrid[:shape_native[0], :shape_native[1]] + y_scaled = (y - centres_scaled[0]) * pixel_scales[0] + x_scaled = (x - centres_scaled[1]) * pixel_scales[1] - centres_scaled = mask_2d_centres_from( - shape_native=mask_2d.shape, pixel_scales=pixel_scales, centre=centre - ) + # Rotate the coordinates by the angle (counterclockwise) - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - y_scaled = (y - centres_scaled[0]) * pixel_scales[0] - x_scaled = (x - centres_scaled[1]) * pixel_scales[1] + r_scaled = np.sqrt(x_scaled**2 + y_scaled**2) - r_scaled_elliptical = elliptical_radius_from( - y_scaled, x_scaled, angle, axis_ratio - ) + theta_rotated = np.arctan2(y_scaled, x_scaled) + np.radians(angle) - if r_scaled_elliptical <= major_axis_radius: - mask_2d[y, x] = False + y_scaled_elliptical = r_scaled * np.sin(theta_rotated) + x_scaled_elliptical = r_scaled * np.cos(theta_rotated) - return mask_2d + # Compute the elliptical radius + r_scaled_elliptical = np.sqrt(x_scaled_elliptical**2 + (y_scaled_elliptical / axis_ratio)**2) + + return ~(r_scaled_elliptical <= major_axis_radius) @numba_util.jit() From 36b6d3509a1ac326ea94644f21ef97944d91ee7f Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 19:16:52 +0000 Subject: [PATCH 07/17] mask_2d_elliptical_annular_from --- autoarray/mask/mask_2d_util.py | 104 ++++++++++++--------------------- 1 file changed, 37 insertions(+), 67 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index a73e6c909..732a13db1 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -130,44 +130,6 @@ def mask_2d_circular_annular_from( ) -@numba_util.jit() -def elliptical_radius_from( - y_scaled: float, x_scaled: float, angle: float, axis_ratio: float -) -> float: - """ - Returns the elliptical radius of an ellipse from its (y,x) scaled centre, rotation angle `angle` defined in degrees - counter-clockwise from the positive x-axis and its axis-ratio. - - This is used by the function `mask_elliptical_from` to determine the radius of every (y,x) coordinate in elliptical - units when deciding if it is within the mask. - - Parameters - ---------- - y_scaled - The scaled y coordinate in Cartesian coordinates which is converted to elliptical coordinates. - x_scaled - The scaled x coordinate in Cartesian coordinates which is converted to elliptical coordinates. - angle - The rotation angle in degrees counter-clockwise from the positive x-axis - axis_ratio - The axis-ratio of the ellipse (minor axis / major axis). - - Returns - ------- - float - The radius of the input scaled (y,x) coordinate on the ellipse's ellipitcal coordinate system. - """ - r_scaled = np.sqrt(x_scaled**2 + y_scaled**2) - - theta_rotated = np.arctan2(y_scaled, x_scaled) + np.radians(angle) - - y_scaled_elliptical = r_scaled * np.sin(theta_rotated) - x_scaled_elliptical = r_scaled * np.cos(theta_rotated) - - return np.sqrt(x_scaled_elliptical**2.0 + (y_scaled_elliptical / axis_ratio) ** 2.0) - - - def mask_2d_elliptical_from( shape_native: Tuple[int, int], pixel_scales: Tuple[float, float], @@ -209,7 +171,7 @@ def mask_2d_elliptical_from( """ centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) - y, x = np.ogrid[:shape_native[0], :shape_native[1]] + y, x = np.ogrid[: shape_native[0], : shape_native[1]] y_scaled = (y - centres_scaled[0]) * pixel_scales[0] x_scaled = (x - centres_scaled[1]) * pixel_scales[1] @@ -223,15 +185,16 @@ def mask_2d_elliptical_from( x_scaled_elliptical = r_scaled * np.cos(theta_rotated) # Compute the elliptical radius - r_scaled_elliptical = np.sqrt(x_scaled_elliptical**2 + (y_scaled_elliptical / axis_ratio)**2) + r_scaled_elliptical = np.sqrt( + x_scaled_elliptical**2 + (y_scaled_elliptical / axis_ratio) ** 2 + ) return ~(r_scaled_elliptical <= major_axis_radius) -@numba_util.jit() def mask_2d_elliptical_annular_from( shape_native: Tuple[int, int], - pixel_scales: ty.PixelScales, + pixel_scales: Tuple[float, float], inner_major_axis_radius: float, inner_axis_ratio: float, inner_phi: float, @@ -277,38 +240,45 @@ def mask_2d_elliptical_annular_from( Examples -------- mask = mask_elliptical_annuli_from( - shape=(10, 10), pixel_scales=0.1, - inner_major_axis_radius=0.5, inner_axis_ratio=0.5, inner_phi=45.0, - outer_major_axis_radius=1.5, outer_axis_ratio=0.8, outer_phi=90.0, - centre=(0.0, 0.0)) + shape=(10, 10), pixel_scales=(0.1, 0.1), + inner_major_axis_radius=0.5, inner_axis_ratio=0.5, inner_phi=45.0, + outer_major_axis_radius=1.5, outer_axis_ratio=0.8, outer_phi=90.0, + centre=(0.0, 0.0)) """ + centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) - mask_2d = np.full(shape_native, True) - - centres_scaled = mask_2d_centres_from( - shape_native=mask_2d.shape, pixel_scales=pixel_scales, centre=centre - ) + y, x = np.ogrid[: shape_native[0], : shape_native[1]] + y_scaled = (y - centres_scaled[0]) * pixel_scales[0] + x_scaled = (x - centres_scaled[1]) * pixel_scales[1] - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - y_scaled = (y - centres_scaled[0]) * pixel_scales[0] - x_scaled = (x - centres_scaled[1]) * pixel_scales[1] + # Rotate the coordinates for the inner annulus + r_scaled_inner = np.sqrt(x_scaled**2 + y_scaled**2) + theta_rotated_inner = np.arctan2(y_scaled, x_scaled) + np.radians(inner_phi) + y_scaled_elliptical_inner = r_scaled_inner * np.sin(theta_rotated_inner) + x_scaled_elliptical_inner = r_scaled_inner * np.cos(theta_rotated_inner) - inner_r_scaled_elliptical = elliptical_radius_from( - y_scaled, x_scaled, inner_phi, inner_axis_ratio - ) + # Compute the elliptical radius for the inner annulus + r_scaled_elliptical_inner = np.sqrt( + x_scaled_elliptical_inner**2 + + (y_scaled_elliptical_inner / inner_axis_ratio) ** 2 + ) - outer_r_scaled_elliptical = elliptical_radius_from( - y_scaled, x_scaled, outer_phi, outer_axis_ratio - ) + # Rotate the coordinates for the outer annulus + r_scaled_outer = np.sqrt(x_scaled**2 + y_scaled**2) + theta_rotated_outer = np.arctan2(y_scaled, x_scaled) + np.radians(outer_phi) + y_scaled_elliptical_outer = r_scaled_outer * np.sin(theta_rotated_outer) + x_scaled_elliptical_outer = r_scaled_outer * np.cos(theta_rotated_outer) - if ( - inner_r_scaled_elliptical >= inner_major_axis_radius - and outer_r_scaled_elliptical <= outer_major_axis_radius - ): - mask_2d[y, x] = False + # Compute the elliptical radius for the outer annulus + r_scaled_elliptical_outer = np.sqrt( + x_scaled_elliptical_outer**2 + + (y_scaled_elliptical_outer / outer_axis_ratio) ** 2 + ) - return mask_2d + return ~( + (r_scaled_elliptical_inner >= inner_major_axis_radius) + & (r_scaled_elliptical_outer <= outer_major_axis_radius) + ) def mask_2d_via_pixel_coordinates_from( From ec8ca607beb5d966d4bb9c04794e994ed79f4ee4 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 19:18:43 +0000 Subject: [PATCH 08/17] simplify tests to not include centre --- test_autoarray/mask/test_mask_2d_util.py | 166 ++--------------------- 1 file changed, 14 insertions(+), 152 deletions(-) diff --git a/test_autoarray/mask/test_mask_2d_util.py b/test_autoarray/mask/test_mask_2d_util.py index 344f8a1c1..befc358be 100644 --- a/test_autoarray/mask/test_mask_2d_util.py +++ b/test_autoarray/mask/test_mask_2d_util.py @@ -5,20 +5,6 @@ import pytest -def test__total_edge_pixels_from_mask(): - mask_2d = np.array( - [ - [True, True, True, True, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, True, True, True, True], - ] - ) - - assert util.mask_2d.total_edge_pixels_from(mask_2d=mask_2d) == 8 - - def test__mask_2d_circular_from(): mask = util.mask_2d.mask_2d_circular_from( shape_native=(3, 3), pixel_scales=(1.0, 1.0), radius=0.5 @@ -71,26 +57,6 @@ def test__mask_2d_circular_from(): ) ).all() - -def test__mask_2d_circular_from__input_centre(): - mask = util.mask_2d.mask_2d_circular_from( - shape_native=(3, 3), pixel_scales=(3.0, 3.0), radius=0.5, centre=(-3, 0) - ) - - assert mask.shape == (3, 3) - assert ( - mask == np.array([[True, True, True], [True, True, True], [True, False, True]]) - ).all() - - mask = util.mask_2d.mask_2d_circular_from( - shape_native=(3, 3), pixel_scales=(3.0, 3.0), radius=0.5, centre=(0.0, 3.0) - ) - - assert mask.shape == (3, 3) - assert ( - mask == np.array([[True, True, True], [True, True, False], [True, True, True]]) - ).all() - mask = util.mask_2d.mask_2d_circular_from( shape_native=(3, 3), pixel_scales=(3.0, 3.0), radius=0.5, centre=(3, 3) ) @@ -183,40 +149,6 @@ def test__mask_2d_circular_annular_from(): ) ).all() - -def test__mask_2d_circular_annular_from__input_centre(): - mask = util.mask_2d.mask_2d_circular_annular_from( - shape_native=(3, 3), - pixel_scales=(3.0, 3.0), - inner_radius=0.5, - outer_radius=9.0, - centre=(3.0, 0.0), - ) - - assert mask.shape == (3, 3) - assert ( - mask - == np.array( - [[False, True, False], [False, False, False], [False, False, False]] - ) - ).all() - - mask = util.mask_2d.mask_2d_circular_annular_from( - shape_native=(3, 3), - pixel_scales=(3.0, 3.0), - inner_radius=0.5, - outer_radius=9.0, - centre=(0.0, 3.0), - ) - - assert mask.shape == (3, 3) - assert ( - mask - == np.array( - [[False, False, False], [False, False, True], [False, False, False]] - ) - ).all() - mask = util.mask_2d.mask_2d_circular_annular_from( shape_native=(3, 3), pixel_scales=(3.0, 3.0), @@ -333,34 +265,6 @@ def test__mask_2d_elliptical_from(): ) ).all() - -def test__mask_2d_elliptical_from__include_centre(): - mask = util.mask_2d.mask_2d_elliptical_from( - shape_native=(3, 3), - pixel_scales=(3.0, 3.0), - major_axis_radius=4.8, - axis_ratio=0.1, - angle=45.0, - centre=(-3.0, 0.0), - ) - - assert ( - mask == np.array([[True, True, True], [True, True, False], [True, False, True]]) - ).all() - - mask = util.mask_2d.mask_2d_elliptical_from( - shape_native=(3, 3), - pixel_scales=(3.0, 3.0), - major_axis_radius=4.8, - axis_ratio=0.1, - angle=45.0, - centre=(0.0, 3.0), - ) - - assert ( - mask == np.array([[True, True, True], [True, True, False], [True, False, True]]) - ).all() - mask = util.mask_2d.mask_2d_elliptical_from( shape_native=(3, 3), pixel_scales=(3.0, 3.0), @@ -551,62 +455,6 @@ def test__mask_2d_elliptical_annular_from(): ) ).all() - -def test__mask_2d_elliptical_annular_from__include_centre(): - mask = util.mask_2d.mask_2d_elliptical_annular_from( - shape_native=(7, 5), - pixel_scales=(1.0, 1.0), - inner_major_axis_radius=1.0, - inner_axis_ratio=0.1, - inner_phi=0.0, - outer_major_axis_radius=2.0, - outer_axis_ratio=0.1, - outer_phi=90.0, - centre=(-1.0, 0.0), - ) - - assert ( - mask - == np.array( - [ - [True, True, True, True, True], - [True, True, True, True, True], - [True, True, False, True, True], - [True, True, False, True, True], - [True, True, True, True, True], - [True, True, False, True, True], - [True, True, False, True, True], - ] - ) - ).all() - - mask = util.mask_2d.mask_2d_elliptical_annular_from( - shape_native=(7, 5), - pixel_scales=(1.0, 1.0), - inner_major_axis_radius=1.0, - inner_axis_ratio=0.1, - inner_phi=0.0, - outer_major_axis_radius=2.0, - outer_axis_ratio=0.1, - outer_phi=90.0, - centre=(0.0, 1.0), - ) - - assert ( - mask - == np.array( - [ - [True, True, True, True, True], - [True, True, True, False, True], - [True, True, True, False, True], - [True, True, True, True, True], - [True, True, True, False, True], - [True, True, True, False, True], - [True, True, True, True, True], - ] - ) - ).all() - mask = util.mask_2d.mask_2d_elliptical_annular_from( shape_native=(7, 5), pixel_scales=(1.0, 1.0), @@ -899,6 +747,20 @@ def test__mask_1d_indexes_from(): assert masked_slim[-1] == 48 +def test__total_edge_pixels_from_mask(): + mask_2d = np.array( + [ + [True, True, True, True, True], + [True, False, False, False, True], + [True, False, False, False, True], + [True, False, False, False, True], + [True, True, True, True, True], + ] + ) + + assert util.mask_2d.total_edge_pixels_from(mask_2d=mask_2d) == 8 + + def test__edge_1d_indexes_from(): mask = np.array( [ From 943ff407f32ecf12ddb01eb4ba19f29bc2bcbfce Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 15 Mar 2025 20:10:31 +0000 Subject: [PATCH 09/17] blurring_mask_2d_from --- autoarray/mask/mask_2d_util.py | 42 +++++++++--------------- test_autoarray/mask/test_mask_2d_util.py | 14 -------- 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 732a13db1..9b0157309 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -312,7 +312,9 @@ def mask_2d_via_pixel_coordinates_from( return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) -@numba_util.jit() +from scipy.ndimage import convolve + + def blurring_mask_2d_from( mask_2d: np.ndarray, kernel_shape_native: Tuple[int, int] ) -> np.ndarray: @@ -348,32 +350,20 @@ def blurring_mask_2d_from( """ - blurring_mask_2d = np.full(mask_2d.shape, True) + # Create a (3, 3) kernel of ones + kernel = np.ones(kernel_shape_native, dtype=np.uint8) - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - if not mask_2d[y, x]: - for y1 in range( - (-kernel_shape_native[0] + 1) // 2, - (kernel_shape_native[0] + 1) // 2, - ): - for x1 in range( - (-kernel_shape_native[1] + 1) // 2, - (kernel_shape_native[1] + 1) // 2, - ): - if ( - 0 <= x + x1 <= mask_2d.shape[1] - 1 - and 0 <= y + y1 <= mask_2d.shape[0] - 1 - ): - if mask_2d[y + y1, x + x1]: - blurring_mask_2d[y + y1, x + x1] = False - else: - raise exc.MaskException( - "setup_blurring_mask extends beyond the edge " - "of the mask - pad the datas array before masking" - ) - - return blurring_mask_2d + # Convolve the mask with the kernel, applying logical AND to maintain 'True' regions + convolved_mask = convolve(mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0) + + # We want to return the mask where the convolved value is the full kernel size (i.e., 9 for a 3x3 kernel) + result_mask = convolved_mask == np.prod(kernel_shape_native) + + blurring_mask = ~mask_2d + result_mask + + print(blurring_mask * convolved_mask) + + return blurring_mask @numba_util.jit() diff --git a/test_autoarray/mask/test_mask_2d_util.py b/test_autoarray/mask/test_mask_2d_util.py index befc358be..9e1718abd 100644 --- a/test_autoarray/mask/test_mask_2d_util.py +++ b/test_autoarray/mask/test_mask_2d_util.py @@ -522,20 +522,6 @@ def test__blurring_mask_2d_from(): ) ).all() - mask = np.array( - [ - [True, True, True, True, True, True, True], - [True, True, True, True, True, True, True], - [True, True, True, True, True, True, True], - [True, True, True, False, True, True, True], - [True, True, True, True, True, True, True], - [True, True, True, True, True, True, True], - [True, True, True, True, True, True, True], - ] - ) - - blurring_mask = util.mask_2d.blurring_mask_2d_from(mask, kernel_shape_native=(3, 3)) - mask = np.array( [ [True, True, True, True, True, True, True], From 0290b84919e5240e049c045d167f8b29572c52e6 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 30 Mar 2025 15:32:27 +0100 Subject: [PATCH 10/17] check on blurring mask now works --- autoarray/mask/mask_2d_util.py | 70 ++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 9b0157309..f78e9b852 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -1,4 +1,5 @@ import numpy as np +from scipy.ndimage import convolve from typing import Tuple import warnings @@ -308,12 +309,58 @@ def mask_2d_via_pixel_coordinates_from( if buffer == 0: return mask_2d - else: - return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) + return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) -from scipy.ndimage import convolve +import numpy as np + + +def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]: + """ + Compute the minimum 1D distance in the y and x directions from any False value at the mask's extreme positions + (leftmost, rightmost, topmost, bottommost) to its closest edge. + Parameters + ---------- + mask + A 2D boolean array where False represents the unmasked region. + + Returns + ------- + The smallest distances of any extreme False value to the nearest edge in the vertical (y) and horizontal (x) + directions. + + Examples + -------- + >>> mask = np.array([ + ... [ True, True, True, True], + ... [ True, False, False, True], + ... [ True, False, True, True], + ... [ True, True, True, True] + ... ]) + >>> min_false_distance_to_edge(mask) + (1, 1) + """ + false_indices = np.column_stack(np.where(mask == False)) + + if false_indices.size == 0: + raise ValueError("No False values found in the mask.") + + leftmost = false_indices[np.argmin(false_indices[:, 1])] + rightmost = false_indices[np.argmax(false_indices[:, 1])] + topmost = false_indices[np.argmin(false_indices[:, 0])] + bottommost = false_indices[np.argmax(false_indices[:, 0])] + + height, width = mask.shape + + # Compute distances to respective edges + left_dist = leftmost[1] # Distance to left edge (column index) + right_dist = width - 1 - rightmost[1] # Distance to right edge + top_dist = topmost[0] # Distance to top edge (row index) + bottom_dist = height - 1 - bottommost[0] # Distance to bottom edge + + # Return the minimum distance to an edge + return min(top_dist, bottom_dist), min(left_dist, right_dist) def blurring_mask_2d_from( mask_2d: np.ndarray, kernel_shape_native: Tuple[int, int] @@ -350,19 +397,26 @@ def blurring_mask_2d_from( """ - # Create a (3, 3) kernel of ones + y_distance, x_distance = min_false_distance_to_edge(mask_2d) + + y_kernel_distance = (kernel_shape_native[0]) // 2 + x_kernel_distance = (kernel_shape_native[1]) // 2 + + if (y_distance < y_kernel_distance) or (x_distance < x_kernel_distance): + + raise exc.MaskException( + "The input mask is too small for the kernel shape. " + "Please pad the mask before computing the blurring mask." + ) + kernel = np.ones(kernel_shape_native, dtype=np.uint8) - # Convolve the mask with the kernel, applying logical AND to maintain 'True' regions convolved_mask = convolve(mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0) - # We want to return the mask where the convolved value is the full kernel size (i.e., 9 for a 3x3 kernel) result_mask = convolved_mask == np.prod(kernel_shape_native) blurring_mask = ~mask_2d + result_mask - print(blurring_mask * convolved_mask) - return blurring_mask From 744e2ed96d56ebb5754fa503cea881c1aca2d8c6 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 30 Mar 2025 15:47:07 +0100 Subject: [PATCH 11/17] improve mask 2d util docs --- autoarray/mask/mask_2d_util.py | 211 ++++++++++++++++++++++----------- 1 file changed, 145 insertions(+), 66 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index f78e9b852..4eca6a758 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -34,12 +34,19 @@ def mask_2d_centres_from( Examples -------- - centres_scaled = mask_2d_centres_from(shape_native=(5, 5), pixel_scales=(0.5, 0.5), centre=(0.0, 0.0)) + >>> centres_scaled = mask_2d_centres_from(shape_native=(5, 5), pixel_scales=(0.5, 0.5), centre=(0.0, 0.0)) + >>> print(centres_scaled) + (0.0, 0.0) """ - return ( - 0.5 * (shape_native[0] - 1) - (centre[0] / pixel_scales[0]), - 0.5 * (shape_native[1] - 1) + (centre[1] / pixel_scales[1]), - ) + + # Calculate scaled y-coordinate by centering and adjusting for pixel scale + y_scaled = 0.5 * (shape_native[0] - 1) - (centre[0] / pixel_scales[0]) + + # Calculate scaled x-coordinate by centering and adjusting for pixel scale + x_scaled = 0.5 * (shape_native[1] - 1) + (centre[1] / pixel_scales[1]) + + # Return the scaled (y, x) coordinates + return (y_scaled, x_scaled) def mask_2d_circular_from( @@ -70,16 +77,23 @@ def mask_2d_circular_from( Examples -------- - mask = mask_2d_circular_from(shape_native=(10, 10), pixel_scales=(0.1, 0.1), radius=0.5, centre=(0.0, 0.0)) + >>> mask = mask_2d_circular_from(shape_native=(10, 10), pixel_scales=(0.1, 0.1), radius=0.5, centre=(0.0, 0.0)) """ + + # Get scaled coordinates of the mask center centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) + # Create a grid of y, x indices for the mask y, x = np.ogrid[: shape_native[0], : shape_native[1]] + + # Scale the y and x indices based on pixel scales y_scaled = (y - centres_scaled[0]) * pixel_scales[0] x_scaled = (x - centres_scaled[1]) * pixel_scales[1] + # Compute squared distances from the center for each pixel distances_squared = x_scaled**2 + y_scaled**2 + # Return a mask with True for pixels outside the circle and False for inside return distances_squared >= radius**2 @@ -114,18 +128,25 @@ def mask_2d_circular_annular_from( Examples -------- - mask = mask_2d_circular_annular_from( - shape_native=(10, 10), pixel_scales=(0.1, 0.1), inner_radius=0.5, outer_radius=1.5, centre=(0.0, 0.0) - ) + >>> mask = mask_2d_circular_annular_from( + >>> shape_native=(10, 10), pixel_scales=(0.1, 0.1), inner_radius=0.5, outer_radius=1.5, centre=(0.0, 0.0) + >>> ) """ + + # Get scaled coordinates of the mask center centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) + # Create grid of y, x indices for the mask y, x = np.ogrid[: shape_native[0], : shape_native[1]] + + # Scale the y and x indices based on pixel scales y_scaled = (y - centres_scaled[0]) * pixel_scales[0] x_scaled = (x - centres_scaled[1]) * pixel_scales[1] + # Compute squared distances from the center for each pixel distances_squared = x_scaled**2 + y_scaled**2 + # Return the mask where pixels are unmasked between inner and outer radii return ~( (distances_squared >= inner_radius**2) & (distances_squared <= outer_radius**2) ) @@ -166,30 +187,31 @@ def mask_2d_elliptical_from( Examples -------- - mask = mask_2d_elliptical_from( - shape_native=(10, 10), pixel_scales=(0.1, 0.1), major_axis_radius=0.5, axis_ratio=0.5, angle=45.0, centre=(0.0, 0.0) - ) + >>> mask = mask_2d_elliptical_from( + >>> shape_native=(10, 10), pixel_scales=(0.1, 0.1), major_axis_radius=0.5, axis_ratio=0.5, angle=45.0, centre=(0.0, 0.0) + >>> ) """ + + # Get scaled coordinates of the mask center centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) + # Create grid of y, x indices for the mask y, x = np.ogrid[: shape_native[0], : shape_native[1]] + + # Scale the y and x indices based on pixel scales y_scaled = (y - centres_scaled[0]) * pixel_scales[0] x_scaled = (x - centres_scaled[1]) * pixel_scales[1] - # Rotate the coordinates by the angle (counterclockwise) - + # Compute the rotated coordinates and elliptical radius r_scaled = np.sqrt(x_scaled**2 + y_scaled**2) - theta_rotated = np.arctan2(y_scaled, x_scaled) + np.radians(angle) - y_scaled_elliptical = r_scaled * np.sin(theta_rotated) x_scaled_elliptical = r_scaled * np.cos(theta_rotated) - - # Compute the elliptical radius r_scaled_elliptical = np.sqrt( x_scaled_elliptical**2 + (y_scaled_elliptical / axis_ratio) ** 2 ) + # Return the mask where pixels are outside the elliptical region return ~(r_scaled_elliptical <= major_axis_radius) @@ -231,7 +253,7 @@ def mask_2d_elliptical_annular_from( The rotation angle of the outer ellipse within which pixels are unmasked, (counter-clockwise from the positive x-axis). centre - The centre of the elliptical annuli used to mask pixels. + The centre of the elliptical annuli used to mask pixels. Returns ------- @@ -240,42 +262,45 @@ def mask_2d_elliptical_annular_from( Examples -------- - mask = mask_elliptical_annuli_from( - shape=(10, 10), pixel_scales=(0.1, 0.1), - inner_major_axis_radius=0.5, inner_axis_ratio=0.5, inner_phi=45.0, - outer_major_axis_radius=1.5, outer_axis_ratio=0.8, outer_phi=90.0, - centre=(0.0, 0.0)) + >>> mask = mask_elliptical_annuli_from( + >>> shape=(10, 10), pixel_scales=(0.1, 0.1), + >>> inner_major_axis_radius=0.5, inner_axis_ratio=0.5, inner_phi=45.0, + >>> outer_major_axis_radius=1.5, outer_axis_ratio=0.8, outer_phi=90.0, + >>> centre=(0.0, 0.0) + >>> ) """ + + # Get scaled coordinates of the mask center centres_scaled = mask_2d_centres_from(shape_native, pixel_scales, centre) + # Create grid of y, x indices for the mask y, x = np.ogrid[: shape_native[0], : shape_native[1]] + + # Scale the y and x indices based on pixel scales y_scaled = (y - centres_scaled[0]) * pixel_scales[0] x_scaled = (x - centres_scaled[1]) * pixel_scales[1] - # Rotate the coordinates for the inner annulus + # Compute and rotate coordinates for inner annulus r_scaled_inner = np.sqrt(x_scaled**2 + y_scaled**2) theta_rotated_inner = np.arctan2(y_scaled, x_scaled) + np.radians(inner_phi) y_scaled_elliptical_inner = r_scaled_inner * np.sin(theta_rotated_inner) x_scaled_elliptical_inner = r_scaled_inner * np.cos(theta_rotated_inner) - - # Compute the elliptical radius for the inner annulus r_scaled_elliptical_inner = np.sqrt( x_scaled_elliptical_inner**2 + (y_scaled_elliptical_inner / inner_axis_ratio) ** 2 ) - # Rotate the coordinates for the outer annulus + # Compute and rotate coordinates for outer annulus r_scaled_outer = np.sqrt(x_scaled**2 + y_scaled**2) theta_rotated_outer = np.arctan2(y_scaled, x_scaled) + np.radians(outer_phi) y_scaled_elliptical_outer = r_scaled_outer * np.sin(theta_rotated_outer) x_scaled_elliptical_outer = r_scaled_outer * np.cos(theta_rotated_outer) - - # Compute the elliptical radius for the outer annulus r_scaled_elliptical_outer = np.sqrt( x_scaled_elliptical_outer**2 + (y_scaled_elliptical_outer / outer_axis_ratio) ** 2 ) + # Return the mask where pixels are outside the inner and outer elliptical annuli return ~( (r_scaled_elliptical_inner >= inner_major_axis_radius) & (r_scaled_elliptical_outer <= outer_major_axis_radius) @@ -283,33 +308,53 @@ def mask_2d_elliptical_annular_from( def mask_2d_via_pixel_coordinates_from( - shape_native: Tuple[int, int], pixel_coordinates: [list], buffer: int = 0 + shape_native: Tuple[int, int], pixel_coordinates: list, buffer: int = 0 ) -> np.ndarray: """ - Returns a mask where all unmasked `False` entries are defined from an input list of list of pixel coordinates. + Returns a mask where all unmasked `False` entries are defined from an input list of 2D pixel coordinates. - These may be buffed via an input ``buffer``, whereby all entries in all 8 neighboring directions by this + These may be buffed via an input `buffer`, whereby all entries in all 8 neighboring directions are buffed by this amount. Parameters ---------- - shape_native (int, int) - The (y,x) shape of the mask in units of pixels. - pixel_coordinates : [[int, int]] - The input lists of 2D pixel coordinates where `False` entries are created. + shape_native + The (y, x) shape of the mask in units of pixels. + pixel_coordinates + The input list of 2D pixel coordinates where `False` entries are created. buffer - All input ``pixel_coordinates`` are buffed with `False` entries in all 8 neighboring directions by this + All input `pixel_coordinates` are buffed with `False` entries in all 8 neighboring directions by this amount. - """ - mask_2d = np.full(shape=shape_native, fill_value=True) + Returns + ------- + np.ndarray + The 2D mask array where all entries in the input pixel coordinates are set to `False`, with optional buffering + applied to the neighboring entries. - for y, x in pixel_coordinates: + Examples + -------- + mask = mask_2d_via_pixel_coordinates_from( + shape_native=(10, 10), + pixel_coordinates=[[1, 2], [3, 4], [5, 6]], + buffer=1 + ) + """ + mask_2d = np.full( + shape=shape_native, fill_value=True + ) # Initialize mask with all True values + + for ( + y, + x, + ) in ( + pixel_coordinates + ): # Loop over input coordinates to set corresponding mask entries to False mask_2d[y, x] = False - if buffer == 0: + if buffer == 0: # If no buffer is specified, return the mask directly return mask_2d - return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) + return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) # Apply buf import numpy as np @@ -317,18 +362,19 @@ def mask_2d_via_pixel_coordinates_from( def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]: """ - Compute the minimum 1D distance in the y and x directions from any False value at the mask's extreme positions + Compute the minimum 1D distance in the y and x directions from any `False` value at the mask's extreme positions (leftmost, rightmost, topmost, bottommost) to its closest edge. Parameters ---------- mask - A 2D boolean array where False represents the unmasked region. + A 2D boolean array where `False` represents the unmasked region. Returns ------- - The smallest distances of any extreme False value to the nearest edge in the vertical (y) and horizontal (x) - directions. + Tuple[int, int] + The smallest distances of any extreme `False` value to the nearest edge in the vertical (y) and horizontal (x) + directions. Examples -------- @@ -341,17 +387,29 @@ def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]: >>> min_false_distance_to_edge(mask) (1, 1) """ - false_indices = np.column_stack(np.where(mask == False)) + false_indices = np.column_stack( + np.where(mask == False) + ) # Find all coordinates where mask is False if false_indices.size == 0: - raise ValueError("No False values found in the mask.") - - leftmost = false_indices[np.argmin(false_indices[:, 1])] - rightmost = false_indices[np.argmax(false_indices[:, 1])] - topmost = false_indices[np.argmin(false_indices[:, 0])] - bottommost = false_indices[np.argmax(false_indices[:, 0])] - - height, width = mask.shape + raise ValueError( + "No False values found in the mask." + ) # Raise error if no False values + + leftmost = false_indices[ + np.argmin(false_indices[:, 1]) + ] # Find the leftmost False coordinate + rightmost = false_indices[ + np.argmax(false_indices[:, 1]) + ] # Find the rightmost False coordinate + topmost = false_indices[ + np.argmin(false_indices[:, 0]) + ] # Find the topmost False coordinate + bottommost = false_indices[ + np.argmax(false_indices[:, 0]) + ] # Find the bottommost False coordinate + + height, width = mask.shape # Get the height and width of the mask # Compute distances to respective edges left_dist = leftmost[1] # Distance to left edge (column index) @@ -359,9 +417,10 @@ def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]: top_dist = topmost[0] # Distance to top edge (row index) bottom_dist = height - 1 - bottommost[0] # Distance to bottom edge - # Return the minimum distance to an edge + # Return the minimum distance to both edges return min(top_dist, bottom_dist), min(left_dist, right_dist) + def blurring_mask_2d_from( mask_2d: np.ndarray, kernel_shape_native: Tuple[int, int] ) -> np.ndarray: @@ -397,27 +456,47 @@ def blurring_mask_2d_from( """ - y_distance, x_distance = min_false_distance_to_edge(mask_2d) + # Get the distance from False values to edges + y_distance, x_distance = min_false_distance_to_edge( + mask_2d + ) - y_kernel_distance = (kernel_shape_native[0]) // 2 - x_kernel_distance = (kernel_shape_native[1]) // 2 + # Compute kernel half-size in y and x direction + y_kernel_distance = ( + kernel_shape_native[0] + ) // 2 + x_kernel_distance = ( + kernel_shape_native[1] + ) // 2 + # Check if mask is too small for the kernel size if (y_distance < y_kernel_distance) or (x_distance < x_kernel_distance): - raise exc.MaskException( "The input mask is too small for the kernel shape. " "Please pad the mask before computing the blurring mask." ) - kernel = np.ones(kernel_shape_native, dtype=np.uint8) + # Create a kernel with the given PSF shape + kernel = np.ones( + kernel_shape_native, dtype=np.uint8 + ) - convolved_mask = convolve(mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0) + # Convolve mask with kernel producing non-zero values around mask False values + convolved_mask = convolve( + mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0 + ) - result_mask = convolved_mask == np.prod(kernel_shape_native) + # Identify pixels that are non-zero and fully covered by kernel + result_mask = convolved_mask == np.prod( + kernel_shape_native + ) - blurring_mask = ~mask_2d + result_mask + # Create the blurring mask by removing False values in original mask + blurring_mask = ( + ~mask_2d + result_mask + ) - return blurring_mask + return blurring_mask @numba_util.jit() From affea87ee786dd0c63ca5bd869a28f294406926a Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 30 Mar 2025 15:52:30 +0100 Subject: [PATCH 12/17] remove mask_2d_via_shape_native_and_native_for_slim --- autoarray/mask/mask_2d_util.py | 45 +----------------------- test_autoarray/mask/test_mask_2d_util.py | 38 -------------------- 2 files changed, 1 insertion(+), 82 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 4eca6a758..f7dd8e2af 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -496,50 +496,7 @@ def blurring_mask_2d_from( ~mask_2d + result_mask ) - return blurring_mask - - -@numba_util.jit() -def mask_2d_via_shape_native_and_native_for_slim( - shape_native: Tuple[int, int], native_for_slim: np.ndarray -) -> np.ndarray: - """ - For a slimmed set of data that was computed by mapping unmasked values from a native 2D array of shape - (total_y_pixels, total_x_pixels), map its slimmed indexes back to the original 2D array to create the - native 2D mask. - - This uses an array 'native_for_slim' of shape [total_masked_pixels[ where each index gives the native 2D pixel - indexes of the slimmed array's unmasked pixels, for example: - - - If native_for_slim[0] = [0,0], the first value of the slimmed array maps to the pixel [0,0] of the native 2D array. - - If native_for_slim[1] = [0,1], the second value of the slimmed array maps to the pixel [0,1] of the native 2D array. - - If native_for_slim[4] = [1,1], the fifth value of the slimmed array maps to the pixel [1,1] of the native 2D array. - - Parameters - ---------- - shape_native - The shape of the 2D array which the pixels are defined on. - native_for_slim - An array describing the native 2D array index that every slimmed array index maps too. - - Returns - ------- - ndarray - A 2D mask array where unmasked values are `False`. - - Examples - -------- - native_for_slim = np.array([[0,1], [1,0], [1,1], [1,2], [2,1]]) - - mask = mask_from(shape=(3,3), native_for_slim=native_for_slim) - """ - - mask = np.ones(shape_native) - - for index in range(len(native_for_slim)): - mask[native_for_slim[index, 0], native_for_slim[index, 1]] = False - - return mask + return blurring_mask @numba_util.jit() diff --git a/test_autoarray/mask/test_mask_2d_util.py b/test_autoarray/mask/test_mask_2d_util.py index 9e1718abd..94779c78d 100644 --- a/test_autoarray/mask/test_mask_2d_util.py +++ b/test_autoarray/mask/test_mask_2d_util.py @@ -668,44 +668,6 @@ def test__blurring_mask_2d_from__mask_extends_beyond_edge_so_raises_mask_excepti util.mask_2d.blurring_mask_2d_from(mask, kernel_shape_native=(5, 5)) -def test__mask_2d_via_shape_native_and_native_for_slim(): - slim_to_native = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) - shape = (2, 2) - - mask = util.mask_2d.mask_2d_via_shape_native_and_native_for_slim( - shape_native=shape, native_for_slim=slim_to_native - ) - - assert (mask == np.array([[False, False], [False, False]])).all() - - slim_to_native = np.array([[0, 0], [0, 1], [1, 0]]) - shape = (2, 2) - - mask = util.mask_2d.mask_2d_via_shape_native_and_native_for_slim( - shape_native=shape, native_for_slim=slim_to_native - ) - - assert (mask == np.array([[False, False], [False, True]])).all() - - slim_to_native = np.array([[0, 0], [0, 1], [1, 0], [2, 0], [2, 1], [2, 3]]) - shape = (3, 4) - - mask = util.mask_2d.mask_2d_via_shape_native_and_native_for_slim( - shape_native=shape, native_for_slim=slim_to_native - ) - - assert ( - mask - == np.array( - [ - [False, False, True, True], - [False, True, True, True], - [False, False, True, False], - ] - ) - ).all() - - def test__mask_1d_indexes_from(): mask = np.array( [ From 4cd2971bb647d304e85dab5e1b2c32961943fef7 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sun, 30 Mar 2025 15:58:36 +0100 Subject: [PATCH 13/17] mask_slim_indexes_from --- autoarray/mask/mask_2d_util.py | 79 ++++++++++++---------------------- 1 file changed, 28 insertions(+), 51 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index f7dd8e2af..2e6b4b0e1 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -457,17 +457,11 @@ def blurring_mask_2d_from( """ # Get the distance from False values to edges - y_distance, x_distance = min_false_distance_to_edge( - mask_2d - ) + y_distance, x_distance = min_false_distance_to_edge(mask_2d) # Compute kernel half-size in y and x direction - y_kernel_distance = ( - kernel_shape_native[0] - ) // 2 - x_kernel_distance = ( - kernel_shape_native[1] - ) // 2 + y_kernel_distance = (kernel_shape_native[0]) // 2 + x_kernel_distance = (kernel_shape_native[1]) // 2 # Check if mask is too small for the kernel size if (y_distance < y_kernel_distance) or (x_distance < x_kernel_distance): @@ -477,29 +471,18 @@ def blurring_mask_2d_from( ) # Create a kernel with the given PSF shape - kernel = np.ones( - kernel_shape_native, dtype=np.uint8 - ) + kernel = np.ones(kernel_shape_native, dtype=np.uint8) # Convolve mask with kernel producing non-zero values around mask False values - convolved_mask = convolve( - mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0 - ) + convolved_mask = convolve(mask_2d.astype(np.uint8), kernel, mode="reflect", cval=0) # Identify pixels that are non-zero and fully covered by kernel - result_mask = convolved_mask == np.prod( - kernel_shape_native - ) + result_mask = convolved_mask == np.prod(kernel_shape_native) # Create the blurring mask by removing False values in original mask - blurring_mask = ( - ~mask_2d + result_mask - ) + return ~mask_2d + result_mask - return blurring_mask - -@numba_util.jit() def mask_slim_indexes_from( mask_2d: np.ndarray, return_masked_indexes: bool = True ) -> np.ndarray: @@ -509,12 +492,12 @@ def mask_slim_indexes_from( For example, for the following ``Mask2D``: :: - [[True, True, True, True] + [[True, True, True, True], [True, False, False, True], [True, False, True, True], [True, True, True, True]] - This has three unmasked (``False`` values) which have the ``slim`` indexes, there ``unmasked_slim`` is: + This has three unmasked (``False`` values) which have the ``slim`` indexes, their ``unmasked_slim`` is: :: [0, 1, 2] @@ -522,36 +505,30 @@ def mask_slim_indexes_from( Parameters ---------- mask_2d - The mask for which the 1D unmasked pixel indexes are computed. + A 2D array representing the mask, where `True` indicates a masked pixel and `False` indicates an unmasked pixel. return_masked_indexes - Whether to return the masked index values (`value=True`) or the unmasked index values (`value=False`). + A boolean flag that determines whether to return indexes of masked (`True`) or unmasked (`False`) pixels. Returns ------- - np.ndarray - The 1D indexes of all unmasked pixels on the mask. - """ - - mask_pixel_total = 0 - - for y in range(0, mask_2d.shape[0]): - for x in range(0, mask_2d.shape[1]): - if mask_2d[y, x] == return_masked_indexes: - mask_pixel_total += 1 - - mask_pixels = np.zeros(mask_pixel_total) - mask_index = 0 - regular_index = 0 + A 1D array of indexes corresponding to either the masked or unmasked pixels in the mask. - for y in range(0, mask_2d.shape[0]): - for x in range(0, mask_2d.shape[1]): - if mask_2d[y, x] == return_masked_indexes: - mask_pixels[mask_index] = regular_index - mask_index += 1 - - regular_index += 1 - - return mask_pixels + Examples + -------- + >>> mask = np.array([[True, True, True, True], + ... [True, False, False, True], + ... [True, False, True, True], + ... [True, True, True, True]]) + >>> mask_slim_indexes_from(mask, return_masked_indexes=True) + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> mask_slim_indexes_from(mask, return_masked_indexes=False) + array([10, 11]) + """ + # Flatten the mask and use np.where to get indexes of either True or False + mask_flat = mask_2d.flatten() + + # Get the indexes where the mask is equal to return_masked_indexes (True or False) + return np.where(mask_flat == return_masked_indexes)[0] @numba_util.jit() From 06a9ec3b94fad0ae247c61b2519087ecc75a258e Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Mon, 31 Mar 2025 19:39:32 +0100 Subject: [PATCH 14/17] edge_1d_indexes_from --- autoarray/mask/mask_2d_util.py | 250 +++++++---------------- test_autoarray/mask/test_mask_2d_util.py | 15 +- 2 files changed, 81 insertions(+), 184 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 2e6b4b0e1..2ba06135c 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -8,6 +8,52 @@ from autoarray import type as ty from autoarray.numpy_wrapper import use_jax, np as jnp +def native_index_for_slim_index_2d_from( + mask_2d: np.ndarray, +) -> np.ndarray: + """ + Returns an array of shape [total_unmasked_pixels] that maps every unmasked pixel to its + corresponding native 2D pixel using its (y,x) pixel indexes. + + For example, for the following ``Mask2D``: + + :: + [[True, True, True, True] + [True, False, False, True], + [True, False, True, True], + [True, True, True, True]] + + This has three unmasked (``False`` values) which have the ``slim`` indexes: + + :: + [0, 1, 2] + + The array ``native_index_for_slim_index_2d`` is therefore: + + :: + [[1,1], [1,2], [2,1]] + + Parameters + ---------- + mask_2d + A 2D array of bools, where `False` values are unmasked. + + Returns + ------- + ndarray + An array that maps pixels from a slimmed array of shape [total_unmasked_pixels] to its native array + of shape [total_pixels, total_pixels]. + + Examples + -------- + mask_2d = np.array([[True, True, True], + [True, False, True] + [True, True, True]]) + + native_index_for_slim_index_2d = native_index_for_slim_index_2d_from(mask_2d=mask_2d) + """ + return jnp.stack(jnp.nonzero(~mask_2d.astype(bool))).T + def mask_2d_centres_from( shape_native: Tuple[int, int], @@ -531,136 +577,56 @@ def mask_slim_indexes_from( return np.where(mask_flat == return_masked_indexes)[0] -@numba_util.jit() -def check_if_edge_pixel(mask_2d: np.ndarray, y: int, x: int) -> bool: - """ - Checks if an input [y,x] pixel on the input `mask` is an edge-pixel. - - An edge pixel is defined as a pixel on the mask which is unmasked (has a `False`) value and at least 1 of its 8 - direct neighbors is masked (is `True`). - - Parameters - ---------- - mask_2d - The mask for which the input pixel is checked if it is an edge pixel. - y - The y pixel coordinate on the mask that is checked for if it is an edge pixel. - x - The x pixel coordinate on the mask that is checked for if it is an edge pixel. - - Returns - ------- - bool - If `True` the pixel on the mask is an edge pixel, else a `False` is returned because it is not. - """ - - if ( - mask_2d[y + 1, x] - or mask_2d[y - 1, x] - or mask_2d[y, x + 1] - or mask_2d[y, x - 1] - or mask_2d[y + 1, x + 1] - or mask_2d[y + 1, x - 1] - or mask_2d[y - 1, x + 1] - or mask_2d[y - 1, x - 1] - ): - return True - else: - return False - - -@numba_util.jit() -def total_edge_pixels_from(mask_2d: np.ndarray) -> int: - """ - Returns the total number of edge-pixels in a mask. - - An edge pixel is defined as a pixel on the mask which is unmasked (has a `False`) value and at least 1 of its 8 - direct neighbors is masked (is `True`). - - Parameters - ---------- - mask_2d - The mask for which the total number of edge pixels is computed. - - Returns - ------- - int - The total number of edge pixels. - """ - - edge_pixel_total = 0 - - for y in range(1, mask_2d.shape[0] - 1): - for x in range(1, mask_2d.shape[1] - 1): - if not mask_2d[y, x]: - if check_if_edge_pixel(mask_2d=mask_2d, y=y, x=x): - edge_pixel_total += 1 - - return edge_pixel_total - - -@numba_util.jit() def edge_1d_indexes_from(mask_2d: np.ndarray) -> np.ndarray: """ Returns a 1D array listing all edge pixel indexes in the mask. - An edge pixel is defined as a pixel on the mask which is unmasked (has a `False`) value and at least 1 of its 8 + An edge pixel is defined as a pixel on the mask which is unmasked (has a `False`) value and at least one of its 8 direct neighbors is masked (is `True`). - For example, for the following ``Mask2D``: - - :: - [[True, True, True, True, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, True, True, True, True]] - - The `edge_slim` indexes (given via ``mask_2d.derive_indexes.edge_slim``) is given by: - - :: - [0, 1, 2, 3, 5, 6, 7, 8] - - Note that index 4 is skipped, which corresponds to the ``False`` value in the centre of the mask, because it - does not neighbor a ``True`` value in any one of the eight neighboring directions and is therefore not at - an edge. - Parameters ---------- mask_2d - The mask for which the 1D edge pixel indexes are computed. + A 2D boolean array where `False` values indicate unmasked pixels. Returns ------- np.ndarray - The 1D indexes of all edge pixels on the mask. - """ - - edge_pixel_total = total_edge_pixels_from(mask_2d) + A 1D array of indexes of all edge pixels on the mask. - edge_pixels = np.zeros(edge_pixel_total) - edge_index = 0 - regular_index = 0 + Examples + -------- + >>> mask = np.array([ + ... [True, True, True, True, True], + ... [True, False, False, True, True], + ... [True, False, False, False, True], + ... [True, True, False, True, True], + ... [True, True, True, True, True] + ... ]) + >>> edge_1d_indexes_from(mask) + array([1, 2, 5, 7, 8, 9]) + """ + # Pad the mask to handle edge cases without index errors + padded_mask = np.pad(mask_2d, pad_width=1, mode='constant', constant_values=True) + + # Identify neighbors in 3x3 regions around each pixel + neighbors = ( + padded_mask[:-2, 1:-1] | padded_mask[2:, 1:-1] | # Up, Down + padded_mask[1:-1, :-2] | padded_mask[1:-1, 2:] | # Left, Right + padded_mask[:-2, :-2] | padded_mask[:-2, 2:] | # Top-left, Top-right + padded_mask[2:, :-2] | padded_mask[2:, 2:] # Bottom-left, Bottom-right + ) - for y in range(1, mask_2d.shape[0] - 1): - for x in range(1, mask_2d.shape[1] - 1): - if not mask_2d[y, x]: - if ( - mask_2d[y + 1, x] - or mask_2d[y - 1, x] - or mask_2d[y, x + 1] - or mask_2d[y, x - 1] - or mask_2d[y + 1, x + 1] - or mask_2d[y + 1, x - 1] - or mask_2d[y - 1, x + 1] - or mask_2d[y - 1, x - 1] - ): - edge_pixels[edge_index] = regular_index - edge_index += 1 + # Identify edge pixels: False values with at least one True neighbor + edge_mask = ~mask_2d & neighbors - regular_index += 1 + # Create an index array where False entries get sequential 1D indices + index_array = np.full(mask_2d.shape, fill_value=-1, dtype=int) + false_indices = np.flatnonzero(~mask_2d) + index_array[~mask_2d] = np.arange(len(false_indices)) - return edge_pixels + # Return the 1D indexes of the edge pixels + return index_array[edge_mask] @numba_util.jit() @@ -911,62 +877,4 @@ def rescaled_mask_2d_from(mask_2d: np.ndarray, rescale_factor: float) -> np.ndar return np.isclose(rescaled_mask_2d, 1) -@numba_util.jit() -def native_index_for_slim_index_2d_from( - mask_2d: np.ndarray, -) -> np.ndarray: - """ - Returns an array of shape [total_unmasked_pixels] that maps every unmasked pixel to its - corresponding native 2D pixel using its (y,x) pixel indexes. - - For example, for the following ``Mask2D``: - - :: - [[True, True, True, True] - [True, False, False, True], - [True, False, True, True], - [True, True, True, True]] - - This has three unmasked (``False`` values) which have the ``slim`` indexes: - - :: - [0, 1, 2] - - The array ``native_index_for_slim_index_2d`` is therefore: - - :: - [[1,1], [1,2], [2,1]] - - Parameters - ---------- - mask_2d - A 2D array of bools, where `False` values are unmasked. - - Returns - ------- - ndarray - An array that maps pixels from a slimmed array of shape [total_unmasked_pixels] to its native array - of shape [total_pixels, total_pixels]. - - Examples - -------- - mask_2d = np.array([[True, True, True], - [True, False, True] - [True, True, True]]) - - native_index_for_slim_index_2d = native_index_for_slim_index_2d_from(mask_2d=mask_2d) - """ - if use_jax: - return jnp.stack(jnp.nonzero(~mask_2d.astype(bool))).T - else: - total_pixels = np.sum(~mask_2d) - native_index_for_slim_index_2d = np.zeros(shape=(total_pixels, 2)) - slim_index = 0 - - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - if not mask_2d[y, x]: - native_index_for_slim_index_2d[slim_index, :] = y, x - slim_index += 1 - return native_index_for_slim_index_2d diff --git a/test_autoarray/mask/test_mask_2d_util.py b/test_autoarray/mask/test_mask_2d_util.py index 94779c78d..240e91f7d 100644 --- a/test_autoarray/mask/test_mask_2d_util.py +++ b/test_autoarray/mask/test_mask_2d_util.py @@ -695,19 +695,6 @@ def test__mask_1d_indexes_from(): assert masked_slim[-1] == 48 -def test__total_edge_pixels_from_mask(): - mask_2d = np.array( - [ - [True, True, True, True, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, False, False, False, True], - [True, True, True, True, True], - ] - ) - - assert util.mask_2d.total_edge_pixels_from(mask_2d=mask_2d) == 8 - def test__edge_1d_indexes_from(): mask = np.array( @@ -724,6 +711,8 @@ def test__edge_1d_indexes_from(): edge_pixels = util.mask_2d.edge_1d_indexes_from(mask_2d=mask) + print(edge_pixels) + assert (edge_pixels == np.array([0])).all() mask = np.array( From 39fc17370adbce2012bc2e31f0a83da176b8a02d Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Mon, 31 Mar 2025 19:55:51 +0100 Subject: [PATCH 15/17] border_slim_indexes_from --- autoarray/mask/mask_2d_util.py | 185 +++++++++++---------------------- 1 file changed, 63 insertions(+), 122 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index 2ba06135c..c8765b93d 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -584,6 +584,24 @@ def edge_1d_indexes_from(mask_2d: np.ndarray) -> np.ndarray: An edge pixel is defined as a pixel on the mask which is unmasked (has a `False`) value and at least one of its 8 direct neighbors is masked (is `True`). + For example, for the following ``Mask2D``: + + :: + [[True, True, True, True, True], + [True, False, False, False, True], + [True, False, False, False, True], + [True, False, False, False, True], + [True, True, True, True, True]] + + The `edge_slim` indexes (given via ``mask_2d.derive_indexes.edge_slim``) is given by: + + :: + [0, 1, 2, 3, 5, 6, 7, 8] + + Note that index 4 is skipped, which corresponds to the ``False`` value in the centre of the mask, because it + does not neighbor a ``True`` value in any one of the eight neighboring directions and is therefore not at + an edge. + Parameters ---------- mask_2d @@ -591,20 +609,19 @@ def edge_1d_indexes_from(mask_2d: np.ndarray) -> np.ndarray: Returns ------- - np.ndarray - A 1D array of indexes of all edge pixels on the mask. + A 1D array of indexes of all edge pixels on the mask. Examples -------- >>> mask = np.array([ ... [True, True, True, True, True], - ... [True, False, False, True, True], ... [True, False, False, False, True], - ... [True, True, False, True, True], + ... [True, False, False, False, True], + ... [True, False, False, False, True], ... [True, True, True, True, True] ... ]) >>> edge_1d_indexes_from(mask) - array([1, 2, 5, 7, 8, 9]) + array([0, 1, 2, 3, 5, 6, 7, 8]) """ # Pad the mask to handle edge cases without index errors padded_mask = np.pad(mask_2d, pad_width=1, mode='constant', constant_values=True) @@ -629,102 +646,12 @@ def edge_1d_indexes_from(mask_2d: np.ndarray) -> np.ndarray: return index_array[edge_mask] -@numba_util.jit() -def check_if_border_pixel( - mask_2d: np.ndarray, edge_pixel_slim: int, native_to_slim: np.ndarray -) -> bool: - """ - Checks if an input [y,x] pixel on the input `mask` is a border-pixel. - - A borders pixel is a pixel which: - - 1) is not fully surrounding by `False` mask values. - 2) Can reach the edge of the array without hitting a masked pixel in one of four directions (upwards, downwards, - left, right). - - The borders pixels are thus pixels which are on the exterior edge of the mask. For example, the inner ring of edge - pixels in an annular mask are edge pixels but not borders pixels. - - Parameters - ---------- - mask_2d - The mask for which the input pixel is checked if it is a border pixel. - edge_pixel_slim - The edge pixel index in 1D that is checked if it is a border pixel (this 1D index is mapped to 2d via the - array `native_index_for_slim_index_2d`). - native_to_slim - An array describing the native 2D array index that every slimmed array index maps too. - - Returns - ------- - bool - If `True` the pixel on the mask is a border pixel, else a `False` is returned because it is not. - """ - edge_pixel_index = int(edge_pixel_slim) - - y = int(native_to_slim[edge_pixel_index, 0]) - x = int(native_to_slim[edge_pixel_index, 1]) - - if ( - np.sum(mask_2d[0:y, x]) == y - or np.sum(mask_2d[y, x : mask_2d.shape[1]]) == mask_2d.shape[1] - x - 1 - or np.sum(mask_2d[y : mask_2d.shape[0], x]) == mask_2d.shape[0] - y - 1 - or np.sum(mask_2d[y, 0:x]) == x - ): - return True - else: - return False - - -@numba_util.jit() -def total_border_pixels_from(mask_2d, edge_pixels, native_to_slim): - """ - Returns the total number of border-pixels in a mask. - - A borders pixel is a pixel which: - - 1) is not fully surrounding by `False` mask values. - 2) Can reach the edge of the array without hitting a masked pixel in one of four directions (upwards, downwards, - left, right). - - The borders pixels are thus pixels which are on the exterior edge of the mask. For example, the inner ring of edge - pixels in an annular mask are edge pixels but not borders pixels. - - Parameters - ---------- - mask_2d - The mask for which the total number of border pixels is computed. - edge_pixel_1d - The edge pixel index in 1D that is checked if it is a border pixel (this 1D index is mapped to 2d via the - array `native_index_for_slim_index_2d`). - native_to_slim - An array describing the 2D array index that every 1D array index maps too. - - Returns - ------- - int - The total number of border pixels. - """ - - border_pixel_total = 0 - - for i in range(edge_pixels.shape[0]): - if check_if_border_pixel(mask_2d, edge_pixels[i], native_to_slim): - border_pixel_total += 1 - - return border_pixel_total - - -@numba_util.jit() def border_slim_indexes_from(mask_2d: np.ndarray) -> np.ndarray: """ - Returns a slim array of shape [total_unmasked_border_pixels] listing all borders pixel indexes in the mask. - - A borders pixel is a pixel which: + Returns a 1D array listing all border pixel indexes in the mask. - 1) is not fully surrounding by `False` mask values. - 2) Can reach the edge of the array without hitting a masked pixel in one of four directions (upwards, downwards, - left, right). + A border pixel is an unmasked pixel (`False` value) that can reach the edge of the mask without encountering + a masked (`True`) pixel in any of the four cardinal directions (up, down, left, right). The borders pixels are thus pixels which are on the exterior edge of the mask. For example, the inner ring of edge pixels in an annular mask are edge pixels but not borders pixels. @@ -753,39 +680,53 @@ def border_slim_indexes_from(mask_2d: np.ndarray) -> np.ndarray: Parameters ---------- mask_2d - The mask for which the slimmed border pixel indexes are calculated. + A 2D boolean array where `False` values indicate unmasked pixels. Returns ------- - np.ndarray - The slimmed indexes of all border pixels on the mask. - """ + A 1D array of indexes of all border pixels on the mask. - edge_pixels = edge_1d_indexes_from(mask_2d=mask_2d) - native_index_for_slim_index_2d = native_index_for_slim_index_2d_from( - mask_2d=mask_2d, - ) + Examples + -------- + >>> mask = np.array([ + ... [True, True, True, True, True, True, True, True, True], + ... [True, False, False, False, False, False, False, False, True], + ... [True, False, True, True, True, True, True, False, True], + ... [True, False, True, False, False, False, True, False, True], + ... [True, False, True, False, True, False, True, False, True], + ... [True, False, True, False, False, False, True, False, True], + ... [True, False, True, True, True, True, True, False, True], + ... [True, False, False, False, False, False, False, False, True], + ... [True, True, True, True, True, True, True, True, True] + ... ]) + >>> border_slim_indexes_from(mask) + array([0, 1, 2, 3, 5, 6, 7, 11, 12, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]) + """ - border_pixel_total = total_border_pixels_from( - mask_2d=mask_2d, - edge_pixels=edge_pixels, - native_to_slim=native_index_for_slim_index_2d, - ) + # Compute cumulative sums along each direction + up_sums = np.cumsum(mask_2d, axis=0) + down_sums = np.cumsum(mask_2d[::-1, :], axis=0)[::-1, :] + left_sums = np.cumsum(mask_2d, axis=1) + right_sums = np.cumsum(mask_2d[:, ::-1], axis=1)[:, ::-1] - border_pixels = np.zeros(border_pixel_total) + # Get mask dimensions + height, width = mask_2d.shape - border_pixel_index = 0 + # Identify border pixels: where the full length in any direction is True + border_mask = ( + (up_sums == np.arange(height)[:, None]) | + (down_sums == np.arange(height - 1, -1, -1)[:, None]) | + (left_sums == np.arange(width)[None, :]) | + (right_sums == np.arange(width - 1, -1, -1)[None, :]) + ) & ~mask_2d - for edge_pixel_index in range(edge_pixels.shape[0]): - if check_if_border_pixel( - mask_2d=mask_2d, - edge_pixel_slim=edge_pixels[edge_pixel_index], - native_to_slim=native_index_for_slim_index_2d, - ): - border_pixels[border_pixel_index] = edge_pixels[edge_pixel_index] - border_pixel_index += 1 + # Create an index array where False entries get sequential 1D indices + index_array = np.full(mask_2d.shape, fill_value=-1, dtype=int) + false_indices = np.flatnonzero(~mask_2d) + index_array[~mask_2d] = np.arange(len(false_indices)) - return border_pixels + # Return the 1D indexes of the border pixels + return index_array[border_mask] @numba_util.jit() From 3066c773b30199df6eea5ba91fb0a01b744dcf0a Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Mon, 31 Mar 2025 20:04:03 +0100 Subject: [PATCH 16/17] all numba decorators removed --- autoarray/mask/mask_2d_util.py | 69 +++++++++++++++--------- test_autoarray/mask/test_mask_2d_util.py | 56 +++++++++---------- 2 files changed, 71 insertions(+), 54 deletions(-) diff --git a/autoarray/mask/mask_2d_util.py b/autoarray/mask/mask_2d_util.py index c8765b93d..9ea4e35c0 100644 --- a/autoarray/mask/mask_2d_util.py +++ b/autoarray/mask/mask_2d_util.py @@ -4,9 +4,7 @@ import warnings from autoarray import exc -from autoarray import numba_util -from autoarray import type as ty -from autoarray.numpy_wrapper import use_jax, np as jnp +from autoarray.numpy_wrapper import np as jnp def native_index_for_slim_index_2d_from( mask_2d: np.ndarray, @@ -402,10 +400,6 @@ def mask_2d_via_pixel_coordinates_from( return mask_2d return buffed_mask_2d_from(mask_2d=mask_2d, buffer=buffer) # Apply buf - -import numpy as np - - def min_false_distance_to_edge(mask: np.ndarray) -> Tuple[int, int]: """ Compute the minimum 1D distance in the y and x directions from any `False` value at the mask's extreme positions @@ -729,38 +723,61 @@ def border_slim_indexes_from(mask_2d: np.ndarray) -> np.ndarray: return index_array[border_mask] -@numba_util.jit() def buffed_mask_2d_from(mask_2d: np.ndarray, buffer: int = 1) -> np.ndarray: """ - Returns a buffed mask from an input mask, where the buffed mask is the input mask but all `False` entries in the - mask are buffed by an integer amount in all 8 surrouning pixels. + Returns a buffed mask from an input mask, where all `False` entries in the mask are "buffed" (set to `False`) + within a specified buffer range in all 8 surrounding directions. + + A "buffed" mask is created by marking all the pixels within a square of size `buffer` around each `False` + entry as `False`. This process simulates expanding the masked region around each `False` entry by the specified + buffer distance. Parameters ---------- mask_2d - The mask whose `False` entries are buffed. + A 2D boolean array where `False` values indicate unmasked pixels. buffer - The number of pixels around each `False` entry that pixel are buffed in all 8 directions. + The number of pixels around each `False` entry that should be buffed in all 8 surrounding directions. + This controls how far the "buffed" region extends from each `False` value. Returns ------- - np.ndarray - The buffed mask. + A new 2D boolean array where all `False` entries in the input mask are expanded by the specified buffer + distance, setting all pixels within the buffer range to `False`. + + Examples + -------- + >>> mask = np.array([ + ... [True, False, True], + ... [False, False, False], + ... [True, True, False] + ... ]) + >>> buffed_mask_2d_from(mask, buffer=1) + array([[False, False, False], + [False, False, False], + [False, False, False]]) """ + # Initialize buffed mask as a copy of the input mask buffed_mask_2d = mask_2d.copy() - for y in range(mask_2d.shape[0]): - for x in range(mask_2d.shape[1]): - if not mask_2d[y, x]: - for y0 in range(y - buffer, y + 1 + buffer): - for x0 in range(x - buffer, x + 1 + buffer): - if ( - y0 >= 0 - and x0 >= 0 - and y0 <= mask_2d.shape[0] - 1 - and x0 <= mask_2d.shape[1] - 1 - ): - buffed_mask_2d[y0, x0] = False + # Identify the coordinates of all False entries + false_coords = np.nonzero(~mask_2d) + + # Create grid of offsets for the neighboring pixels (buffer range) + buffer_range = np.arange(-buffer, buffer + 1) + + # Generate all possible neighbors for each False entry + dy, dx = np.meshgrid(buffer_range, buffer_range, indexing='ij') + neighbors = np.stack([dy.ravel(), dx.ravel()], axis=-1) + + # Calculate all neighboring positions for all False coordinates + all_neighbors = np.add(np.array(false_coords).T[:, np.newaxis], neighbors) + + # Clip the neighbors to stay within the bounds of the mask + valid_neighbors = np.clip(all_neighbors, [0, 0], [mask_2d.shape[0] - 1, mask_2d.shape[1] - 1]) + + # Update the buffed mask: set all the neighbors to False + buffed_mask_2d[valid_neighbors[:, :, 0], valid_neighbors[:, :, 1]] = False return buffed_mask_2d diff --git a/test_autoarray/mask/test_mask_2d_util.py b/test_autoarray/mask/test_mask_2d_util.py index 240e91f7d..bd2ebd84a 100644 --- a/test_autoarray/mask/test_mask_2d_util.py +++ b/test_autoarray/mask/test_mask_2d_util.py @@ -5,6 +5,34 @@ import pytest +def test__native_index_for_slim_index_2d_from(): + mask = np.array([[True, True, True], [True, False, True], [True, True, True]]) + + sub_mask_index_for_sub_mask_1d_index = ( + util.mask_2d.native_index_for_slim_index_2d_from(mask_2d=mask) + ) + + assert (sub_mask_index_for_sub_mask_1d_index == np.array([[1, 1]])).all() + + mask = np.array( + [ + [True, False, True], + [False, False, False], + [True, False, True], + [True, True, False], + ] + ) + + sub_mask_index_for_sub_mask_1d_index = ( + util.mask_2d.native_index_for_slim_index_2d_from(mask_2d=mask) + ) + + assert ( + sub_mask_index_for_sub_mask_1d_index + == np.array([[0, 1], [1, 0], [1, 1], [1, 2], [2, 1], [3, 2]]) + ).all() + + def test__mask_2d_circular_from(): mask = util.mask_2d.mask_2d_circular_from( shape_native=(3, 3), pixel_scales=(1.0, 1.0), radius=0.5 @@ -921,34 +949,6 @@ def test__border_slim_indexes_from(): ).all() -def test__native_index_for_slim_index_2d_from(): - mask = np.array([[True, True, True], [True, False, True], [True, True, True]]) - - sub_mask_index_for_sub_mask_1d_index = ( - util.mask_2d.native_index_for_slim_index_2d_from(mask_2d=mask) - ) - - assert (sub_mask_index_for_sub_mask_1d_index == np.array([[1, 1]])).all() - - mask = np.array( - [ - [True, False, True], - [False, False, False], - [True, False, True], - [True, True, False], - ] - ) - - sub_mask_index_for_sub_mask_1d_index = ( - util.mask_2d.native_index_for_slim_index_2d_from(mask_2d=mask) - ) - - assert ( - sub_mask_index_for_sub_mask_1d_index - == np.array([[0, 1], [1, 0], [1, 1], [1, 2], [2, 1], [3, 2]]) - ).all() - - def test__rescaled_mask_2d_from(): mask = np.array( [ From bbed38d73450fa24588d9d9822125d28e4dee5bf Mon Sep 17 00:00:00 2001 From: Richard Hayes Date: Wed, 2 Apr 2025 10:15:29 +0100 Subject: [PATCH 17/17] Update autoarray/plot/multi_plotters.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- autoarray/plot/multi_plotters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoarray/plot/multi_plotters.py b/autoarray/plot/multi_plotters.py index a58d08c02..84926a416 100644 --- a/autoarray/plot/multi_plotters.py +++ b/autoarray/plot/multi_plotters.py @@ -301,7 +301,7 @@ def output_to_fits( The list of function names that are called to plot the figures on the subplot. figure_name_list The list of figure names that are plotted on the subplot. - filenane + filename The filename that the .fits file is output to. tag_list The list of tags that are used to set the `EXTNAME` of each hdu of the .fits file.