From f3079d5cdda2655e15f698be7b07cf7cd522c6c2 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Fri, 8 Nov 2024 14:39:48 +0000 Subject: [PATCH 1/5] n_i replaced with mask interp --- autogalaxy/ellipse/ellipse/ellipse.py | 39 +++++++++++++------ autogalaxy/gui/scribbler.py | 9 ++++- .../ellipse/ellipse/test_ellipse.py | 19 +++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse.py b/autogalaxy/ellipse/ellipse/ellipse.py index b6d1acd08..954f246d4 100644 --- a/autogalaxy/ellipse/ellipse/ellipse.py +++ b/autogalaxy/ellipse/ellipse/ellipse.py @@ -106,7 +106,7 @@ def total_points_from(self, pixel_scale: float) -> int: return np.min([500, int(np.round(circular_radius_pixels, 1))]) - def angles_from_x0_from(self, pixel_scale: float) -> np.ndarray: + def angles_from_x0_from(self, pixel_scale: float, mask_interp = None) -> np.ndarray: """ Returns the angles from the x-axis to a discrete number of points ranging from 0.0 to 2.0 * np.pi radians. @@ -126,6 +126,9 @@ def angles_from_x0_from(self, pixel_scale: float) -> np.ndarray: ---------- pixel_scale The pixel scale of the data that the ellipse is fitted to and interpolated over. + n_i + The number of points on the ellipse which hit a masked regions and cannot be computed, where this + value is used to change the range of angles computed. Returns ------- @@ -135,7 +138,7 @@ def angles_from_x0_from(self, pixel_scale: float) -> np.ndarray: return np.linspace(0.0, 2.0 * np.pi, total_points)[:-1] - def ellipse_radii_from_major_axis_from(self, pixel_scale: float) -> np.ndarray: + def ellipse_radii_from_major_axis_from(self, pixel_scale: float, mask_interp = None) -> np.ndarray: """ Returns the distance from the centre of the ellipse to every point on the ellipse, which are called the ellipse radii. @@ -147,13 +150,16 @@ def ellipse_radii_from_major_axis_from(self, pixel_scale: float) -> np.ndarray: ---------- pixel_scale The pixel scale of the data that the ellipse is fitted to and interpolated over. + n_i + The number of points on the ellipse which hit a masked regions and cannot be computed, where this + value is used to change the range of angles computed. Returns ------- The ellipse radii from the major-axis of the ellipse. """ - angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale) + angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, mask_interp=mask_interp) return np.divide( self.major_axis * self.minor_axis, @@ -167,7 +173,7 @@ def ellipse_radii_from_major_axis_from(self, pixel_scale: float) -> np.ndarray: ), ) - def x_from_major_axis_from(self, pixel_scale: float) -> np.ndarray: + def x_from_major_axis_from(self, pixel_scale: float, mask_interp = None) -> np.ndarray: """ Returns the x-coordinates of the points on the ellipse, starting from the x-coordinate of the major-axis of the ellipse after rotation by its `angle` and moving counter-clockwise. @@ -176,21 +182,24 @@ def x_from_major_axis_from(self, pixel_scale: float) -> np.ndarray: ---------- pixel_scale The pixel scale of the data that the ellipse is fitted to and interpolated over. + n_i + The number of points on the ellipse which hit a masked regions and cannot be computed, where this + value is used to change the range of angles computed. Returns ------- The x-coordinates of the points on the ellipse. """ - angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale) + angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, mask_interp=mask_interp) ellipse_radii_from_major_axis = self.ellipse_radii_from_major_axis_from( - pixel_scale=pixel_scale + pixel_scale=pixel_scale, mask_interp=mask_interp ) return ellipse_radii_from_major_axis * np.cos(angles_from_x0) + self.centre[1] def y_from_major_axis_from( - self, pixel_scale: float, flip_y: bool = False + self, pixel_scale: float, flip_y: bool = False, mask_interp = None ) -> np.ndarray: """ Returns the y-coordinates of the points on the ellipse, starting from the y-coordinate of the major-axis @@ -209,14 +218,17 @@ def y_from_major_axis_from( The pixel scale of the data that the ellipse is fitted to and interpolated over. flip_y If True, the y-coordinates are flipped to match the convention of the y-axis increasing downwards in 2D data. + n_i + The number of points on the ellipse which hit a masked regions and cannot be computed, where this + value is used to change the range of angles computed. Returns ------- The y-coordinates of the points on the ellipse. """ - angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale) + angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, mask_interp=mask_interp) ellipse_radii_from_major_axis = self.ellipse_radii_from_major_axis_from( - pixel_scale=pixel_scale + pixel_scale=pixel_scale, mask_interp=mask_interp ) if flip_y: @@ -230,7 +242,7 @@ def y_from_major_axis_from( ) def points_from_major_axis_from( - self, pixel_scale: float, flip_y: bool = False + self, pixel_scale: float, flip_y: bool = False, mask_interp = None ) -> np.ndarray: """ Returns the (y,x) coordinates of the points on the ellipse, starting from the major-axis of the ellipse @@ -246,14 +258,17 @@ def points_from_major_axis_from( ---------- pixel_scale The pixel scale of the data that the ellipse is fitted to and interpolated over. + n_i + The number of points on the ellipse which hit a masked regions and cannot be computed, where this + value is used to change the range of angles computed. Returns ------- The (y,x) coordinates of the points on the ellipse. """ - x = self.x_from_major_axis_from(pixel_scale=pixel_scale) - y = self.y_from_major_axis_from(pixel_scale=pixel_scale, flip_y=flip_y) + x = self.x_from_major_axis_from(pixel_scale=pixel_scale, mask_interp=mask_interp) + y = self.y_from_major_axis_from(pixel_scale=pixel_scale, flip_y=flip_y, mask_interp=mask_interp) idx = np.logical_or(np.isnan(x), np.isnan(y)) if np.sum(idx) > 0.0: diff --git a/autogalaxy/gui/scribbler.py b/autogalaxy/gui/scribbler.py index d8183f504..6a0eba91f 100644 --- a/autogalaxy/gui/scribbler.py +++ b/autogalaxy/gui/scribbler.py @@ -37,7 +37,14 @@ def __init__( plt.imshow(image, interpolation="none") else: norm = cmap.norm_from(array=image) - plt.imshow(image, cmap=cmap.config_dict["cmap"], norm=norm) + + if cmap.config_dict["cmap"] == "default": + from matplotlib.colors import LinearSegmentedColormap + from autoarray.plot.wrap.segmentdata import segmentdata + + cmap = LinearSegmentedColormap(name="default", segmentdata=segmentdata) + + plt.imshow(image, cmap=cmap, norm=norm) plt.axis([0, image.shape[1], image.shape[0], 0]) plt.axis("off") # if title: diff --git a/test_autogalaxy/ellipse/ellipse/test_ellipse.py b/test_autogalaxy/ellipse/ellipse/test_ellipse.py index 1b99a13ee..c6d6e604b 100644 --- a/test_autogalaxy/ellipse/ellipse/test_ellipse.py +++ b/test_autogalaxy/ellipse/ellipse/test_ellipse.py @@ -82,6 +82,25 @@ def test__angles_from_x0(): ) +def test__angles_from_x0__with_n_i(): + ellipse = ag.Ellipse(centre=(0.0, 0.0), ell_comps=(0.0, 0.0), major_axis=1.0) + + angles = ellipse.angles_from_x0_from(pixel_scale=1.0, n_i=0) + + print(angles) + + angles = ellipse.angles_from_x0_from(pixel_scale=1.0, n_i=1) + + print(angles) + + jghjhjhg + + assert ellipse.angles_from_x0_from(pixel_scale=1.0)[0] == pytest.approx(0.0, 1.0e-4) + assert ellipse.angles_from_x0_from(pixel_scale=1.0)[1] == pytest.approx( + 1.25663706143, 1.0e-4 + ) + + def test__x_from_major_axis(): ellipse = ag.Ellipse(centre=(0.0, 0.0), ell_comps=(0.0, 0.0), major_axis=1.0) From 3f20e58a1a874eba37cad9e3327d486173d2c700 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Mon, 11 Nov 2024 13:00:17 +0000 Subject: [PATCH 2/5] mask mid implementation --- autogalaxy/ellipse/plot/fit_ellipse_plotters.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py index 022641f79..503d076f5 100644 --- a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py +++ b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py @@ -92,7 +92,11 @@ def figures_2d( ellipse_list.append(aa.Grid2DIrregular.from_yx_1d(y=y, x=x)) - visuals_2d = self.get_visuals_2d() + Visuals2D(lines=ellipse_list) + visuals_2d = self.get_visuals_2d() + Visuals2D( + positions=ellipse_list, + lines=ellipse_list + ) + self.mat_plot_2d.plot_array( array=self.fit_list[0].data, From 7322ad6fc3c7544083b689b66fb86fcb39121d8b Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Mon, 25 Nov 2024 20:40:07 +0000 Subject: [PATCH 3/5] implementation seems to work but buggy --- autogalaxy/ellipse/ellipse/ellipse.py | 26 ++-- autogalaxy/ellipse/fit_ellipse.py | 127 +++++++++++------- autogalaxy/ellipse/model/analysis.py | 10 +- autogalaxy/ellipse/model/plotter_interface.py | 6 +- .../ellipse/ellipse/test_ellipse.py | 20 +-- test_autogalaxy/ellipse/test_fit_ellipse.py | 86 +++++++++++- 6 files changed, 189 insertions(+), 86 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse.py b/autogalaxy/ellipse/ellipse/ellipse.py index 954f246d4..0befe169a 100644 --- a/autogalaxy/ellipse/ellipse/ellipse.py +++ b/autogalaxy/ellipse/ellipse/ellipse.py @@ -106,7 +106,7 @@ def total_points_from(self, pixel_scale: float) -> int: return np.min([500, int(np.round(circular_radius_pixels, 1))]) - def angles_from_x0_from(self, pixel_scale: float, mask_interp = None) -> np.ndarray: + def angles_from_x0_from(self, pixel_scale: float, n_i : int = 0) -> np.ndarray: """ Returns the angles from the x-axis to a discrete number of points ranging from 0.0 to 2.0 * np.pi radians. @@ -136,9 +136,9 @@ def angles_from_x0_from(self, pixel_scale: float, mask_interp = None) -> np.ndar """ total_points = self.total_points_from(pixel_scale) - return np.linspace(0.0, 2.0 * np.pi, total_points)[:-1] + return np.linspace(0.0, 2.0 * np.pi, total_points + n_i)[:-1] - def ellipse_radii_from_major_axis_from(self, pixel_scale: float, mask_interp = None) -> np.ndarray: + def ellipse_radii_from_major_axis_from(self, pixel_scale: float, n_i : int = 0) -> np.ndarray: """ Returns the distance from the centre of the ellipse to every point on the ellipse, which are called the ellipse radii. @@ -159,7 +159,7 @@ def ellipse_radii_from_major_axis_from(self, pixel_scale: float, mask_interp = N The ellipse radii from the major-axis of the ellipse. """ - angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, mask_interp=mask_interp) + angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, n_i=n_i) return np.divide( self.major_axis * self.minor_axis, @@ -173,7 +173,7 @@ def ellipse_radii_from_major_axis_from(self, pixel_scale: float, mask_interp = N ), ) - def x_from_major_axis_from(self, pixel_scale: float, mask_interp = None) -> np.ndarray: + def x_from_major_axis_from(self, pixel_scale: float, n_i : int = 0) -> np.ndarray: """ Returns the x-coordinates of the points on the ellipse, starting from the x-coordinate of the major-axis of the ellipse after rotation by its `angle` and moving counter-clockwise. @@ -191,15 +191,15 @@ def x_from_major_axis_from(self, pixel_scale: float, mask_interp = None) -> np.n The x-coordinates of the points on the ellipse. """ - angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, mask_interp=mask_interp) + angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, n_i=n_i) ellipse_radii_from_major_axis = self.ellipse_radii_from_major_axis_from( - pixel_scale=pixel_scale, mask_interp=mask_interp + pixel_scale=pixel_scale, n_i=n_i ) return ellipse_radii_from_major_axis * np.cos(angles_from_x0) + self.centre[1] def y_from_major_axis_from( - self, pixel_scale: float, flip_y: bool = False, mask_interp = None + self, pixel_scale: float, flip_y: bool = False, n_i : int = 0 ) -> np.ndarray: """ Returns the y-coordinates of the points on the ellipse, starting from the y-coordinate of the major-axis @@ -226,9 +226,9 @@ def y_from_major_axis_from( ------- The y-coordinates of the points on the ellipse. """ - angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, mask_interp=mask_interp) + angles_from_x0 = self.angles_from_x0_from(pixel_scale=pixel_scale, n_i=n_i) ellipse_radii_from_major_axis = self.ellipse_radii_from_major_axis_from( - pixel_scale=pixel_scale, mask_interp=mask_interp + pixel_scale=pixel_scale, n_i=n_i ) if flip_y: @@ -242,7 +242,7 @@ def y_from_major_axis_from( ) def points_from_major_axis_from( - self, pixel_scale: float, flip_y: bool = False, mask_interp = None + self, pixel_scale: float, flip_y: bool = False, n_i : int = 0 ) -> np.ndarray: """ Returns the (y,x) coordinates of the points on the ellipse, starting from the major-axis of the ellipse @@ -267,8 +267,8 @@ def points_from_major_axis_from( The (y,x) coordinates of the points on the ellipse. """ - x = self.x_from_major_axis_from(pixel_scale=pixel_scale, mask_interp=mask_interp) - y = self.y_from_major_axis_from(pixel_scale=pixel_scale, flip_y=flip_y, mask_interp=mask_interp) + x = self.x_from_major_axis_from(pixel_scale=pixel_scale, n_i=n_i) + y = self.y_from_major_axis_from(pixel_scale=pixel_scale, flip_y=flip_y, n_i=n_i) idx = np.logical_or(np.isnan(x), np.isnan(y)) if np.sum(idx) > 0.0: diff --git a/autogalaxy/ellipse/fit_ellipse.py b/autogalaxy/ellipse/fit_ellipse.py index 6ea2ba94c..10ea3e0ae 100644 --- a/autogalaxy/ellipse/fit_ellipse.py +++ b/autogalaxy/ellipse/fit_ellipse.py @@ -56,9 +56,40 @@ def points_from_major_axis_from(self, flip_y: bool = False) -> np.ndarray: The (y,x) coordinates on the ellipse where the interpolation occurs. """ points = self.ellipse.points_from_major_axis_from( - pixel_scale=self.dataset.pixel_scales[0], flip_y=flip_y + pixel_scale=self.dataset.pixel_scales[0], flip_y=flip_y, ) + if self.interp.mask_interp is not None: + + i_total = 300 + + total_points_required = points.shape[0] + + for i in range(1, i_total + 1): + + total_points = points.shape[0] + total_points_masked = np.sum(self.interp.mask_interp(points) > 0) + + if total_points_required == total_points - total_points_masked: + continue + + points = self.ellipse.points_from_major_axis_from(pixel_scale=self.dataset.pixel_scales[0], + flip_y=flip_y, n_i=i) + + if i == i_total: + + raise ValueError( + """ + The code has attempted to add over 1000 points to the ellipse and still not found a set of points that + do not hit the mask with the expected number of points. + + This is likely due to the mask being too large or a strange geometry, and the code is unable to find a + set of points that do not hit the mask. + """ + ) + + points = points[self.interp.mask_interp(points) == 0] + if self.multipole_list is not None: for multipole in self.multipole_list: points = multipole.points_perturbed_from( @@ -80,51 +111,51 @@ def _points_from_major_axis(self) -> np.ndarray: """ return self.points_from_major_axis_from() - @property - def mask_interp(self) -> np.ndarray: - """ - Returns the mask values of the dataset that the ellipse fits, which are computed by overlaying the ellipse over - the 2D data and performing a 2D interpolation at discrete (y,x) coordinates on the ellipse on the dataset's - mask. - - When an input (y,x) coordinate intepolates only unmasked values (`data.mask=False`) the intepolatred value - is 0.0, where if it interpolates one or a masked value (`data.mask=True`), the interpolated value is positive. - To mask all values which interpolate a masked value, all interpolated values above 1 and converted to `True`. - - This mask is used to remove these pixels from a fit and evaluate how many ellipse points are used for each - ellipse fit. - - The (y,x) coordinates on the ellipse where the interpolation occurs are computed in the - `points_from_major_axis` property of the `Ellipse` class, with the documentation describing how these points - are computed. - - Returns - ------- - The data values of the ellipse fits, computed via a 2D interpolation of where the ellipse - overlaps the data. - """ - return self.interp.mask_interp(self._points_from_major_axis) > 0.0 - - @property - def total_points_interp(self) -> int: - """ - Returns the total number of points used to interpolate the data and noise-map values of the ellipse. - - For example, if the ellipse spans 10 pixels, the total number of points will be 10. - - The calculation removes points if one or more interpolated value uses a masked value, meaning the interpolation - is not reliable. - - The (y,x) coordinates on the ellipse where the interpolation occurs are computed in the - `points_from_major_axis` property of the `Ellipse` class, with the documentation describing how these points - are computed. - - Returns - ------- - The noise-map values of the ellipse fits, computed via a 2D interpolation of where the ellipse - overlaps the noise-map. - """ - return self.data_interp[np.invert(self.mask_interp)].shape[0] + # @property + # def mask_interp(self) -> np.ndarray: + # """ + # Returns the mask values of the dataset that the ellipse fits, which are computed by overlaying the ellipse over + # the 2D data and performing a 2D interpolation at discrete (y,x) coordinates on the ellipse on the dataset's + # mask. + # + # When an input (y,x) coordinate intepolates only unmasked values (`data.mask=False`) the intepolatred value + # is 0.0, where if it interpolates one or a masked value (`data.mask=True`), the interpolated value is positive. + # To mask all values which interpolate a masked value, all interpolated values above 1 and converted to `True`. + # + # This mask is used to remove these pixels from a fit and evaluate how many ellipse points are used for each + # ellipse fit. + # + # The (y,x) coordinates on the ellipse where the interpolation occurs are computed in the + # `points_from_major_axis` property of the `Ellipse` class, with the documentation describing how these points + # are computed. + # + # Returns + # ------- + # The data values of the ellipse fits, computed via a 2D interpolation of where the ellipse + # overlaps the data. + # """ + # return self.interp.mask_interp(self._points_from_major_axis) > 0.0 + + # @property + # def total_points_interp(self) -> int: + # """ + # Returns the total number of points used to interpolate the data and noise-map values of the ellipse. + # + # For example, if the ellipse spans 10 pixels, the total number of points will be 10. + # + # The calculation removes points if one or more interpolated value uses a masked value, meaning the interpolation + # is not reliable. + # + # The (y,x) coordinates on the ellipse where the interpolation occurs are computed in the + # `points_from_major_axis` property of the `Ellipse` class, with the documentation describing how these points + # are computed. + # + # Returns + # ------- + # The noise-map values of the ellipse fits, computed via a 2D interpolation of where the ellipse + # overlaps the noise-map. + # """ + # return self.data_interp[np.invert(self.mask_interp)].shape[0] @property def data_interp(self) -> aa.ArrayIrregular: @@ -146,7 +177,7 @@ def data_interp(self) -> aa.ArrayIrregular: """ data = self.interp.data_interp(self._points_from_major_axis) - data[self.mask_interp] = np.nan + # data[self.mask_interp] = np.nan return aa.ArrayIrregular(values=data) @@ -170,7 +201,7 @@ def noise_map_interp(self) -> aa.ArrayIrregular: """ noise_map = self.interp.noise_map_interp(self._points_from_major_axis) - noise_map[self.mask_interp] = np.nan + # noise_map[self.mask_interp] = np.nan return aa.ArrayIrregular(values=noise_map) diff --git a/autogalaxy/ellipse/model/analysis.py b/autogalaxy/ellipse/model/analysis.py index 20a904e7a..346e5330a 100644 --- a/autogalaxy/ellipse/model/analysis.py +++ b/autogalaxy/ellipse/model/analysis.py @@ -11,6 +11,8 @@ from autogalaxy.ellipse.model.result import ResultEllipse from autogalaxy.ellipse.model.visualizer import VisualizerEllipse +from autogalaxy import exc + logger = logging.getLogger(__name__) logger.setLevel(level="INFO") @@ -73,9 +75,11 @@ def log_likelihood_function(self, instance: af.ModelInstance) -> float: float The log likelihood indicating how well this model instance fitted the imaging data. """ - fit_list = self.fit_list_from(instance=instance) - - return sum(fit.log_likelihood for fit in fit_list) + try: + fit_list = self.fit_list_from(instance=instance) + return sum(fit.log_likelihood for fit in fit_list) + except ValueError as e: + raise exc.FitException from e def fit_list_from(self, instance: af.ModelInstance) -> List[FitEllipse]: """ diff --git a/autogalaxy/ellipse/model/plotter_interface.py b/autogalaxy/ellipse/model/plotter_interface.py index bce0ecdf6..2fa5925ca 100644 --- a/autogalaxy/ellipse/model/plotter_interface.py +++ b/autogalaxy/ellipse/model/plotter_interface.py @@ -94,7 +94,11 @@ def should_plot(name): fit_list=fit_list, mat_plot_2d=mat_plot_2d, include_2d=self.include_2d ) - fit_plotter.figures_2d(data=should_plot("data")) + try: + fit_plotter.figures_2d(data=should_plot("data")) + except ValueError: + print(fit_plotter.fit_list[0].ellipse.major_axis) + print(fit_plotter.fit_list[0].ellipse.ell_comps) if should_plot("data_no_ellipse"): fit_plotter.figures_2d( diff --git a/test_autogalaxy/ellipse/ellipse/test_ellipse.py b/test_autogalaxy/ellipse/ellipse/test_ellipse.py index c6d6e604b..57a863401 100644 --- a/test_autogalaxy/ellipse/ellipse/test_ellipse.py +++ b/test_autogalaxy/ellipse/ellipse/test_ellipse.py @@ -82,25 +82,6 @@ def test__angles_from_x0(): ) -def test__angles_from_x0__with_n_i(): - ellipse = ag.Ellipse(centre=(0.0, 0.0), ell_comps=(0.0, 0.0), major_axis=1.0) - - angles = ellipse.angles_from_x0_from(pixel_scale=1.0, n_i=0) - - print(angles) - - angles = ellipse.angles_from_x0_from(pixel_scale=1.0, n_i=1) - - print(angles) - - jghjhjhg - - assert ellipse.angles_from_x0_from(pixel_scale=1.0)[0] == pytest.approx(0.0, 1.0e-4) - assert ellipse.angles_from_x0_from(pixel_scale=1.0)[1] == pytest.approx( - 1.25663706143, 1.0e-4 - ) - - def test__x_from_major_axis(): ellipse = ag.Ellipse(centre=(0.0, 0.0), ell_comps=(0.0, 0.0), major_axis=1.0) @@ -171,3 +152,4 @@ def test__points_from_major_axis(): assert ellipse.points_from_major_axis_from(pixel_scale=1.0)[1][0] == pytest.approx( -0.2123224755, 1.0e-4 ) + diff --git a/test_autogalaxy/ellipse/test_fit_ellipse.py b/test_autogalaxy/ellipse/test_fit_ellipse.py index 7a5918802..d3ab3abfd 100644 --- a/test_autogalaxy/ellipse/test_fit_ellipse.py +++ b/test_autogalaxy/ellipse/test_fit_ellipse.py @@ -30,8 +30,8 @@ def make_imaging_lh_masked(imaging_lh): mask=[ [True, True, True, True, True, True, True], [True, True, True, True, True, True, True], - [True, True, False, False, False, True, True], - [True, True, False, True, False, True, True], + [True, True, False, False, True, True, True], + [True, True, False, False, True, True, True], [True, True, False, False, False, True, True], [True, True, True, True, True, True, True], [True, True, True, True, True, True, True], @@ -215,3 +215,85 @@ def test__log_likelihood(imaging_lh, imaging_lh_masked): fit = ag.FitEllipse(dataset=imaging_lh_masked, ellipse=ellipse_0) assert fit.log_likelihood == pytest.approx(0.0, 1.0e-4) + + +def test__points_from_major_axis_from__with_mask_interp_but_mask_all_false(imaging_lh, imaging_lh_masked): + + ellipse_0 = ag.Ellipse(centre=(0.0, 0.0), ell_comps=(0.0, 0.0), major_axis=1.0) + + # fit = ag.FitEllipse(dataset=imaging_lh, ellipse=ellipse_0) + # points = fit.points_from_major_axis_from() + + fit = ag.FitEllipse(dataset=imaging_lh_masked, ellipse=ellipse_0) + points_masked = fit.points_from_major_axis_from() + + assert points.shape[0] == points_masked.shape[0] == 5 + + fff + + + print(fit.points_from_major_axis_from()) + ffff + + assert fit.mask_interp == pytest.approx([False, True, True, True, True], 1.0e-4) + + ellipse = ag.Ellipse(centre=(0.0, 0.0), ell_comps=(0.0, 0.0), major_axis=1.0) + + points = ellipse.points_from_major_axis_from(pixel_scale=1.0) + + print(points) + + data = ag.Array2D.ones( + shape_native=(3, 3), + pixel_scales=1.0 + ) + noise_map = ag.Array2D.ones(shape_native=(3, 3), pixel_scales=1.0) + + mask = ag.Mask2D( + mask=np.array( + [[False, False, False], [False, False, False], [False, False, False]] + ), + pixel_scales=1.0, + ) + + dataset = ag.Imaging(data=data, noise_map=noise_map) + dataset = dataset.apply_mask(mask=mask) + interp = ag.DatasetInterp(dataset=dataset) + + assert ellipse.points_from_major_axis_from(pixel_scale=1.0, mask_interp=interp.mask_interp)[0][1] == pytest.approx( + 1.0, 1.0e-4 + ) + assert ellipse.points_from_major_axis_from(pixel_scale=1.0, mask_interp=interp.mask_interp)[0][0] == pytest.approx( + 0.0, 1.0e-4 + ) + + +def test__points_from_major_axis_from__with_mask_interp_but_mask_removes_points(): + ellipse = ag.Ellipse(centre=(0.0, 0.0), ell_comps=(0.0, 0.0), major_axis=1.0) + + data = ag.Array2D.ones( + shape_native=(3, 3), + pixel_scales=1.0 + ) + noise_map = ag.Array2D.ones(shape_native=(3, 3), pixel_scales=1.0) + + mask = ag.Mask2D( + mask=np.array( + [[False, False, False], [True, False, False], [False, False, True]] + ), + pixel_scales=1.0, + ) + + dataset = ag.Imaging(data=data, noise_map=noise_map) + dataset = dataset.apply_mask(mask=mask) + interp = ag.DatasetInterp(dataset=dataset) + + points = ellipse.points_from_major_axis_from(pixel_scale=1.0, mask_interp=interp.mask_interp) + + assert len(points) == 5 + assert points[1][1] == pytest.approx( + 1.0, 1.0e-4 + ) + # assert ellipse.points_from_major_axis_from(pixel_scale=1.0, mask_interp=interp.mask_interp)[0][0] == pytest.approx( + # 0.0, 1.0e-4 + # ) From 3f95ce991364a2abbda5b1a9db663fd3c66b7fdc Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Mon, 25 Nov 2024 21:45:00 +0000 Subject: [PATCH 4/5] move flip all the way up to visualization --- autogalaxy/ellipse/ellipse/ellipse.py | 20 +++---------------- autogalaxy/ellipse/fit_ellipse.py | 9 +++------ .../ellipse/plot/fit_ellipse_plotters.py | 4 ++-- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse.py b/autogalaxy/ellipse/ellipse/ellipse.py index 0befe169a..1578c9bac 100644 --- a/autogalaxy/ellipse/ellipse/ellipse.py +++ b/autogalaxy/ellipse/ellipse/ellipse.py @@ -199,7 +199,7 @@ def x_from_major_axis_from(self, pixel_scale: float, n_i : int = 0) -> np.ndarra return ellipse_radii_from_major_axis * np.cos(angles_from_x0) + self.centre[1] def y_from_major_axis_from( - self, pixel_scale: float, flip_y: bool = False, n_i : int = 0 + self, pixel_scale: float, n_i : int = 0 ) -> np.ndarray: """ Returns the y-coordinates of the points on the ellipse, starting from the y-coordinate of the major-axis @@ -209,15 +209,10 @@ def y_from_major_axis_from( that the convention of the y-axis increasing upwards is followed, meaning that `ell_comps` adopt the same definition as used for evaluating light profiles in PyAutoGalaxy. - When plotting the ellipses, y coordinates must be flipped to match the convention of the y-axis increasing - downwards in 2D data, which is performed by setting `flip_y=True`. - Parameters ---------- pixel_scale The pixel scale of the data that the ellipse is fitted to and interpolated over. - flip_y - If True, the y-coordinates are flipped to match the convention of the y-axis increasing downwards in 2D data. n_i The number of points on the ellipse which hit a masked regions and cannot be computed, where this value is used to change the range of angles computed. @@ -231,18 +226,12 @@ def y_from_major_axis_from( pixel_scale=pixel_scale, n_i=n_i ) - if flip_y: - return ( - ellipse_radii_from_major_axis * np.sin(angles_from_x0) + self.centre[0] - ) - return ( -1.0 * (ellipse_radii_from_major_axis * np.sin(angles_from_x0)) - self.centre[0] ) - def points_from_major_axis_from( - self, pixel_scale: float, flip_y: bool = False, n_i : int = 0 + def points_from_major_axis_from(self, pixel_scale: float, n_i : int = 0, ) -> np.ndarray: """ Returns the (y,x) coordinates of the points on the ellipse, starting from the major-axis of the ellipse @@ -251,9 +240,6 @@ def points_from_major_axis_from( This is the format inputs into the inteprolation functions which match the ellipse to 2D data and enable us to determine how well the ellipse represents the data. - When plotting the ellipses, y coordinates must be flipped to match the convention of the y-axis increasing - downwards in 2D data, which is performed by setting `flip_y=True`. - Parameters ---------- pixel_scale @@ -268,7 +254,7 @@ def points_from_major_axis_from( """ x = self.x_from_major_axis_from(pixel_scale=pixel_scale, n_i=n_i) - y = self.y_from_major_axis_from(pixel_scale=pixel_scale, flip_y=flip_y, n_i=n_i) + y = self.y_from_major_axis_from(pixel_scale=pixel_scale, n_i=n_i) idx = np.logical_or(np.isnan(x), np.isnan(y)) if np.sum(idx) > 0.0: diff --git a/autogalaxy/ellipse/fit_ellipse.py b/autogalaxy/ellipse/fit_ellipse.py index 10ea3e0ae..237417349 100644 --- a/autogalaxy/ellipse/fit_ellipse.py +++ b/autogalaxy/ellipse/fit_ellipse.py @@ -39,7 +39,7 @@ def interp(self) -> DatasetInterp: """ return DatasetInterp(dataset=self.dataset) - def points_from_major_axis_from(self, flip_y: bool = False) -> np.ndarray: + def points_from_major_axis_from(self) -> np.ndarray: """ Returns the (y,x) coordinates on the ellipse that are used to interpolate the data and noise-map values. @@ -48,15 +48,12 @@ def points_from_major_axis_from(self, flip_y: bool = False) -> np.ndarray: If multipole components are used, the points are also perturbed by the multipole components. - When plotting the ellipses, y coordinates must be flipped to match the convention of the y-axis increasing - downwards in 2D data, which is performed by setting `flip_y=True`. - Returns ------- The (y,x) coordinates on the ellipse where the interpolation occurs. """ points = self.ellipse.points_from_major_axis_from( - pixel_scale=self.dataset.pixel_scales[0], flip_y=flip_y, + pixel_scale=self.dataset.pixel_scales[0], ) if self.interp.mask_interp is not None: @@ -74,7 +71,7 @@ def points_from_major_axis_from(self, flip_y: bool = False) -> np.ndarray: continue points = self.ellipse.points_from_major_axis_from(pixel_scale=self.dataset.pixel_scales[0], - flip_y=flip_y, n_i=i) + n_i=i) if i == i_total: diff --git a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py index 503d076f5..aac66bf1b 100644 --- a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py +++ b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py @@ -85,10 +85,10 @@ def figures_2d( ellipse_list = [] for fit in self.fit_list: - points = fit.points_from_major_axis_from(flip_y=True) + points = fit.points_from_major_axis_from() x = points[:, 1] - y = points[:, 0] + y = points[:, 0] * -1.0 # flip for plot ellipse_list.append(aa.Grid2DIrregular.from_yx_1d(y=y, x=x)) From 512830e4d6bdcf2b4034cc6674dcb9fe8c74d7a9 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 14 Dec 2024 14:34:16 +0000 Subject: [PATCH 5/5] need to clean up --- autogalaxy/convert.py | 4 ++-- autogalaxy/profiles/mass/total/power_law_multipole.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/autogalaxy/convert.py b/autogalaxy/convert.py index 3364b1812..127d7d4ea 100644 --- a/autogalaxy/convert.py +++ b/autogalaxy/convert.py @@ -269,7 +269,7 @@ def multipole_k_m_and_phi_m_from( The normalization and angle are given by: .. math:: - \phi^{\rm mass}_m = \arctan{\frac{\epsilon_{\rm 2}^{\rm mp}}{\epsilon_{\rm 2}^{\rm mp}}}, \, \, + \phi^{\rm mass}_m = \frac{1}{m} \arctan{\frac{\epsilon_{\rm 2}^{\rm mp}}{\epsilon_{\rm 1}^{\rm mp}}}, \, \, k^{\rm mass}_m = \sqrt{{\epsilon_{\rm 1}^{\rm mp}}^2 + {\epsilon_{\rm 2}^{\rm mp}}^2} \, . The conversion depends on the multipole order `m`, to ensure that all possible rotationally symmetric @@ -307,7 +307,7 @@ def multipole_comps_from(k_m: float, phi_m: float, m: int) -> Tuple[float, float Returns the multipole component parameters from their normalization value `k_m` and angle `phi`. .. math:: - \phi^{\rm mass}_m = \arctan{\frac{\epsilon_{\rm 2}^{\rm mp}}{\epsilon_{\rm 2}^{\rm mp}}}, \, \, + \phi^{\rm mass}_m = \frac{1}{m} \arctan{\frac{\epsilon_{\rm 2}^{\rm mp}}{\epsilon_{\rm 1}^{\rm mp}}}, \, \, k^{\rm mass}_m = \sqrt{{\epsilon_{\rm 1}^{\rm mp}}^2 + {\epsilon_{\rm 2}^{\rm mp}}^2} \, . The conversion depends on the multipole order `m`, to ensure that all possible rotationally symmetric diff --git a/autogalaxy/profiles/mass/total/power_law_multipole.py b/autogalaxy/profiles/mass/total/power_law_multipole.py index da5c5cc8a..663713462 100644 --- a/autogalaxy/profiles/mass/total/power_law_multipole.py +++ b/autogalaxy/profiles/mass/total/power_law_multipole.py @@ -68,7 +68,7 @@ def __init__( :math: (\epsilon_{\rm 1}^{\rm mp}\,\epsilon_{\rm 2}^{\rm mp}), which are given by: .. math:: - \phi^{\rm mass}_m = \arctan{\frac{\epsilon_{\rm 2}^{\rm mp}}{\epsilon_{\rm 2}^{\rm mp}}}, \, \, + \phi^{\rm mass}_m = \frac{1}{m} \arctan{\frac{\epsilon_{\rm 2}^{\rm mp}}{\epsilon_{\rm 1}^{\rm mp}}}, \, \, k^{\rm mass}_m = \sqrt{{\epsilon_{\rm 1}^{\rm mp}}^2 + {\epsilon_{\rm 2}^{\rm mp}}^2} \, . This mass profile is described fully in the following paper: https://arxiv.org/abs/1302.5482