From f3d748c5cb037ec3867cc1a481118d66650bf455 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Wed, 30 Jul 2025 13:47:12 +0100 Subject: [PATCH 01/20] Update subplot_fit_ellipse to use log_10 for data, and remove Fill option from mat_plot_2d --- autogalaxy/ellipse/plot/fit_ellipse_plotters.py | 3 ++- autogalaxy/plot/mat_plot/two_d.py | 2 -- autogalaxy/plot/visuals/two_d.py | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py index bc5f77976..9e1162edb 100644 --- a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py +++ b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py @@ -131,6 +131,7 @@ def subplot_fit_ellipse(self): self.open_subplot_figure(number_subplots=2) + self.mat_plot_2d.use_log10 = True self.figures_2d(data=True) self.figures_2d(ellipse_residuals=True, for_subplot=True) @@ -237,7 +238,7 @@ def subplot_ellipse_errors(self): visuals_2d=visuals_2d, auto_labels=aplt.AutoLabels( title=f"Ellipse Fit", - filename=f"subhplot_ellipse_errors", + filename=f"subplot_ellipse_errors", ), ) diff --git a/autogalaxy/plot/mat_plot/two_d.py b/autogalaxy/plot/mat_plot/two_d.py index 82f6de8b9..f9216771c 100644 --- a/autogalaxy/plot/mat_plot/two_d.py +++ b/autogalaxy/plot/mat_plot/two_d.py @@ -25,7 +25,6 @@ def __init__( output: Optional[aplt.Output] = None, array_overlay: Optional[aplt.ArrayOverlay] = None, contour: Optional[aplt.Contour] = None, - fill: Optional[aplt.Fill] = None, grid_scatter: Optional[aplt.GridScatter] = None, grid_plot: Optional[aplt.GridPlot] = None, vector_yx_quiver: Optional[aplt.VectorYXQuiver] = None, @@ -194,7 +193,6 @@ def __init__( xlabel=xlabel, text=text, annotate=annotate, - fill=fill, output=output, origin_scatter=origin_scatter, mask_scatter=mask_scatter, diff --git a/autogalaxy/plot/visuals/two_d.py b/autogalaxy/plot/visuals/two_d.py index 3d9e425cf..49f422979 100644 --- a/autogalaxy/plot/visuals/two_d.py +++ b/autogalaxy/plot/visuals/two_d.py @@ -17,7 +17,6 @@ def __init__( mesh_grid: aa.Grid2D = None, vectors: aa.VectorYX2DIrregular = None, patches: Union[ptch.Patch] = None, - fill_region: Optional[List] = None, array_overlay: aa.Array2D = None, light_profile_centres: aa.Grid2DIrregular = None, mass_profile_centres: aa.Grid2DIrregular = None, @@ -48,7 +47,6 @@ def __init__( mesh_grid=mesh_grid, vectors=vectors, patches=patches, - fill_region=fill_region, array_overlay=array_overlay, origin=origin, border=border, From b030cf8900b1aa878c80e33b2e2a74d3b8918bb8 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Wed, 30 Jul 2025 13:47:35 +0100 Subject: [PATCH 02/20] Add EllipseMultipoleRelative class --- autogalaxy/__init__.py | 1 + .../ellipse/ellipse/ellipse_multipole.py | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/autogalaxy/__init__.py b/autogalaxy/__init__.py index 738c55b86..2a7c646df 100644 --- a/autogalaxy/__init__.py +++ b/autogalaxy/__init__.py @@ -59,6 +59,7 @@ from .ellipse.dataset_interp import DatasetInterp from .ellipse.ellipse.ellipse import Ellipse from .ellipse.ellipse.ellipse_multipole import EllipseMultipole +from .ellipse.ellipse.ellipse_multipole import EllipseMultipoleRelative from .ellipse.fit_ellipse import FitEllipse from .ellipse.model.analysis import AnalysisEllipse from .operate.image import OperateImage diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index 89ac08517..62cc860fc 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -1,5 +1,6 @@ import numpy as np from typing import Tuple +from autogalaxy.convert import multipole_comps_from, multipole_k_m_and_phi_m_from from autogalaxy.ellipse.ellipse.ellipse import Ellipse @@ -68,3 +69,53 @@ def points_perturbed_from( y = points[:, 0] + (radial * np.sin(angles)) return np.stack(arrays=(y, x), axis=-1) + +class EllipseMultipoleRelative(EllipseMultipole): + def __init__( + self, + m=4, + input_multipole_comps: Tuple[float, float] = (0.0, 0.0), + major_axis=1., + ): + + k, phi = multipole_k_m_and_phi_m_from(multipole_comps=input_multipole_comps, m=m) + k_adjusted = k*major_axis + + adjusted_multipole_comps = multipole_comps_from(k_adjusted, phi, m) + + super().__init__(m, adjusted_multipole_comps) + + self.adjusted_multipole_comps = adjusted_multipole_comps + self.m = m + + def points_perturbed_from( + self, pixel_scale, points, ellipse: Ellipse, n_i: int = 0 + ) -> np.ndarray: + """ + Returns the (y,x) coordinates of the input points, which are perturbed by the multipole of the ellipse. + + Parameters + ---------- + pixel_scale + The pixel scale of the data that the ellipse is fitted to and interpolated over. + points + The (y,x) coordinates of the ellipse that are perturbed by the multipole. + ellipse + The ellipse that is perturbed by the multipole, which is used to compute the angles of the ellipse. + + Returns + ------- + The (y,x) coordinates of the input points, which are perturbed by the multipole. + """ + + angles = ellipse.angles_from_x0_from(pixel_scale=pixel_scale, n_i=n_i) + + radial = np.add( + self.adjusted_multipole_comps[1] * np.cos(self.m * (angles - ellipse.angle_radians)), + self.adjusted_multipole_comps[0] * np.sin(self.m * (angles - ellipse.angle_radians)), + ) + + x = points[:, 1] + (radial * np.cos(angles)) + y = points[:, 0] + (radial * np.sin(angles)) + + return np.stack(arrays=(y, x), axis=-1) \ No newline at end of file From 371d1c46bd5f116d874530dac115114b1668a5d5 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Wed, 30 Jul 2025 13:47:54 +0100 Subject: [PATCH 03/20] Add EllipseMultipoleRelative config --- autogalaxy/config/notation.yaml | 4 ++++ .../priors/ellipse/ellipse_multipole.yaml | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/autogalaxy/config/notation.yaml b/autogalaxy/config/notation.yaml index 264a3c8de..5c7b64b3e 100644 --- a/autogalaxy/config/notation.yaml +++ b/autogalaxy/config/notation.yaml @@ -20,6 +20,8 @@ label: ell_comps_1: \epsilon_{\rm 2} multipole_comps_0: M_{\rm 1} multipole_comps_1: M_{\rm 2} + input_multipole_comps_0: M_{\rm 1} + input_multipole_comps_1: M_{\rm 2} flux: F gamma: \gamma gamma_1: \gamma @@ -96,6 +98,8 @@ label_format: ell_comps_1: '{:.4f}' multipole_comps_0: '{:.4f}' multipole_comps_1: '{:.4f}' + input_multipole_comps_0: '{:.4f}' + input_multipole_comps_1: '{:.4f}' flux: '{:.4e}' gamma: '{:.4f}' inner_coefficient: '{:.4f}' diff --git a/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml b/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml index 232ea447e..74551d143 100644 --- a/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml +++ b/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml @@ -19,3 +19,25 @@ EllipseMultipole: gaussian_limits: lower: -inf upper: inf + +EllipseMultipoleRelative: + input_multipole_comps_0: + type: Uniform + lower_limit: -0.1 + upper_limit: 0.1 + width_modifier: + type: Absolute + value: 0.05 + gaussian_limits: + lower: -inf + upper: inf + input_multipole_comps_1: + type: Uniform + lower_limit: -0.1 + upper_limit: 0.1 + width_modifier: + type: Absolute + value: 0.05 + gaussian_limits: + lower: -inf + upper: inf From 50fcfef1cb452b489b964af365b39ab4abae8749 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Wed, 30 Jul 2025 14:02:46 +0100 Subject: [PATCH 04/20] Fix declaration of input_multipole_comps --- autogalaxy/ellipse/ellipse/ellipse_multipole.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index 62cc860fc..684b42f1a 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -77,7 +77,7 @@ def __init__( input_multipole_comps: Tuple[float, float] = (0.0, 0.0), major_axis=1., ): - + self.input_multipole_comps = input_multipole_comps k, phi = multipole_k_m_and_phi_m_from(multipole_comps=input_multipole_comps, m=m) k_adjusted = k*major_axis From 4f7f40a6b344c63aa35c9bbbe2ffde2de8904605 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 7 Aug 2025 13:18:07 +0100 Subject: [PATCH 05/20] fixed multipole code --- .../ellipse/ellipse/ellipse_multipole.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index 89ac08517..63722d395 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -57,14 +57,18 @@ def points_perturbed_from( The (y,x) coordinates of the input points, which are perturbed by the multipole. """ - angles = ellipse.angles_from_x0_from(pixel_scale=pixel_scale, n_i=n_i) - - radial = np.add( - self.multipole_comps[1] * np.cos(self.m * (angles - ellipse.angle_radians)), - self.multipole_comps[0] * np.sin(self.m * (angles - ellipse.angle_radians)), + # 1) compute cartesian (polar) angle + theta = np.arctan2(points[:,0], points[:,1]) # <- true polar angle + + # 2) multipole in that same frame + delta_theta = self.m * (theta - ellipse.angle_radians) + radial = ( + self.multipole_comps[1] * np.cos(delta_theta) + + self.multipole_comps[0] * np.sin(delta_theta) ) - x = points[:, 1] + (radial * np.cos(angles)) - y = points[:, 0] + (radial * np.sin(angles)) + # 3) perturb along the true radial direction + x = points[:, 1] + radial * np.cos(theta) + y = points[:, 0] + radial * np.sin(theta) - return np.stack(arrays=(y, x), axis=-1) + return np.stack((y, x), axis=-1) From 8399338e0ffe1f93895ff794b5de1fe873276202 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Fri, 8 Aug 2025 13:09:31 +0100 Subject: [PATCH 06/20] Refactor EllipseMultipoleRelative to EllipseMultipoleScaled and add docs to make more intuitive --- .../ellipse/ellipse/ellipse_multipole.py | 50 +++++++++++++++---- autogalaxy/ellipse/fit_ellipse.py | 2 +- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index 241e0e9ef..e049d0713 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -13,7 +13,7 @@ def __init__( multipole_comps: Tuple[float, float] = (0.0, 0.0), ): """ - class representing the multipole of an ellispe with, which is used to perform ellipse fitting to + class representing the multipole of an ellipse, which is used to perform ellipse fitting to 2D data (e.g. an image). The multipole is added to the (y,x) coordinates of an ellipse that are already computed via the `Ellipse` class. @@ -74,22 +74,52 @@ def points_perturbed_from( return np.stack((y, x), axis=-1) -class EllipseMultipoleRelative(EllipseMultipole): +class EllipseMultipoleScaled(EllipseMultipole): def __init__( self, m=4, - input_multipole_comps: Tuple[float, float] = (0.0, 0.0), + scaled_multipole_comps: Tuple[float, float] = (0.0, 0.0), major_axis=1., ): - self.input_multipole_comps = input_multipole_comps - k, phi = multipole_k_m_and_phi_m_from(multipole_comps=input_multipole_comps, m=m) + """ + class representing the multipole of an ellipse, which is used to perform ellipse fitting to + 2D data (e.g. an image). This multipole is fit with its strength held relative to an ellipse with a + major_axis of 1, allowing for a set of ellipse multipoles to be fit at different major axes but with + the same scaled strength k/a. + + The scaled_multipole_comps (for all ellipses) are converted to a k value, which is then reset to + its `true' value for a multipole at the given major axis value, which is then used to perturb an ellipse + as per the normal `EllipseMultipole' class and below. + + The multipole is added to the (y,x) coordinates of an ellipse that are already computed via the `Ellipse` class. + + The addition of the multipole is performed as follows: + + :math: r_m = \sum_{i=1}^{m} \left( a_i \cos(i(\theta - \phi)) + b_i \sin(i(\theta - \phi)) \right) + :math: y_m = r_m \sin(\theta) + :math: x_m = r_m \cos(\theta) + + Where: + + m = The order of the multipole. + r = The radial coordinate of the ellipse perturbed by the multipole. + \phi = The angle of the ellipse. + a = The amplitude of the cosine term of the multipole. + b = The amplitude of the sine term of the multipole. + y = The y-coordinate of the ellipse perturbed by the multipole. + x = The x-coordinate of the ellipse perturbed by the multipole. + """ + + + self.scaled_multipole_comps = scaled_multipole_comps + k, phi = multipole_k_m_and_phi_m_from(multipole_comps=scaled_multipole_comps, m=m) k_adjusted = k*major_axis - adjusted_multipole_comps = multipole_comps_from(k_adjusted, phi, m) + specific_multipole_comps = multipole_comps_from(k_adjusted, phi, m) - super().__init__(m, adjusted_multipole_comps) + super().__init__(m, specific_multipole_comps) - self.adjusted_multipole_comps = adjusted_multipole_comps + self.specific_multipole_comps = specific_multipole_comps self.m = m def points_perturbed_from( @@ -118,8 +148,8 @@ def points_perturbed_from( # 2) multipole in that same frame delta_theta = self.m * (theta - ellipse.angle_radians) radial = ( - self.adjusted_multipole_comps[1] * np.cos(delta_theta) - + self.adjusted_multipole_comps[0] * np.sin(delta_theta) + self.specific_multipole_comps[1] * np.cos(delta_theta) + + self.specific_multipole_comps[0] * np.sin(delta_theta) ) # 3) perturb along the true radial direction diff --git a/autogalaxy/ellipse/fit_ellipse.py b/autogalaxy/ellipse/fit_ellipse.py index 764bd9dbc..1a0f3edaf 100644 --- a/autogalaxy/ellipse/fit_ellipse.py +++ b/autogalaxy/ellipse/fit_ellipse.py @@ -109,7 +109,7 @@ def points_from_major_axis_from(self) -> np.ndarray: raise ValueError( """ - The code has attempted to add over 1000 points to the ellipse and still not found a set of points that + The code has attempted to add over 300 extra 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 From f9d47ab3cb766603b9fa0562ad72b4babac85d6f Mon Sep 17 00:00:00 2001 From: samlange04 Date: Fri, 8 Aug 2025 13:09:57 +0100 Subject: [PATCH 07/20] Add option to disable the data contours in a fit ellipse subplot --- autogalaxy/ellipse/plot/fit_ellipse_plotters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py index 9e1162edb..d8703e314 100644 --- a/autogalaxy/ellipse/plot/fit_ellipse_plotters.py +++ b/autogalaxy/ellipse/plot/fit_ellipse_plotters.py @@ -124,7 +124,7 @@ def figures_2d( for_subplot=for_subplot, ) - def subplot_fit_ellipse(self): + def subplot_fit_ellipse(self, disable_data_contours: bool = False): """ Standard subplot of the attributes of the plotter's `FitEllipse` object. """ @@ -132,7 +132,7 @@ def subplot_fit_ellipse(self): self.open_subplot_figure(number_subplots=2) self.mat_plot_2d.use_log10 = True - self.figures_2d(data=True) + self.figures_2d(data=True, disable_data_contours=disable_data_contours) self.figures_2d(ellipse_residuals=True, for_subplot=True) self.mat_plot_2d.output.subplot_to_figure(auto_filename="subplot_fit_ellipse") From 5f68c047ba313d39aeeb0ff94a8837f475da4dc9 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Wed, 13 Aug 2025 14:45:18 +0100 Subject: [PATCH 08/20] fixes --- autogalaxy/__init__.py | 2 +- autogalaxy/ellipse/ellipse/ellipse.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autogalaxy/__init__.py b/autogalaxy/__init__.py index 2a7c646df..a98271b0c 100644 --- a/autogalaxy/__init__.py +++ b/autogalaxy/__init__.py @@ -59,7 +59,7 @@ from .ellipse.dataset_interp import DatasetInterp from .ellipse.ellipse.ellipse import Ellipse from .ellipse.ellipse.ellipse_multipole import EllipseMultipole -from .ellipse.ellipse.ellipse_multipole import EllipseMultipoleRelative +from .ellipse.ellipse.ellipse_multipole import EllipseMultipoleScaled from .ellipse.fit_ellipse import FitEllipse from .ellipse.model.analysis import AnalysisEllipse from .operate.image import OperateImage diff --git a/autogalaxy/ellipse/ellipse/ellipse.py b/autogalaxy/ellipse/ellipse/ellipse.py index 8986d085f..a1eb347e4 100644 --- a/autogalaxy/ellipse/ellipse/ellipse.py +++ b/autogalaxy/ellipse/ellipse/ellipse.py @@ -41,7 +41,7 @@ class representing an ellispe, which is used to perform ellipse fitting to 2D da @property def circular_radius(self) -> float: """ - The radius of the circle that bounds the ellipse, assuming that the `major_axis` is the radius of the circle. + The circumference of the circle that bounds the ellipse, assuming that the `major_axis` is the radius of the circle. """ return 2.0 * np.pi * np.sqrt((2.0 * self.major_axis**2.0) / 2.0) From 9677b2eedf7e1582b4ed77167f1d2bc74b7fa4e3 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Mon, 13 Oct 2025 23:48:53 +0800 Subject: [PATCH 09/20] Add fixes to align ellipse multipole angle behaviour to EPL multipole behaviour. --- .../ellipse/ellipse/ellipse_multipole.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index e049d0713..d4a47a1c7 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -57,6 +57,11 @@ def points_perturbed_from( ------- The (y,x) coordinates of the input points, which are perturbed by the multipole. """ + symmetry = 360 / self.m + k_orig, phi_orig = multipole_k_m_and_phi_m_from(self.multipole_comps, self.m) + comps_adjusted = multipole_comps_from(k_orig, + symmetry-2*phi_orig+(symmetry-(ellipse.angle-phi_orig)), #Re-align light to match mass + self.m) # 1) compute cartesian (polar) angle theta = np.arctan2(points[:,0], points[:,1]) # <- true polar angle @@ -64,8 +69,8 @@ def points_perturbed_from( # 2) multipole in that same frame delta_theta = self.m * (theta - ellipse.angle_radians) radial = ( - self.multipole_comps[1] * np.cos(delta_theta) - + self.multipole_comps[0] * np.sin(delta_theta) + comps_adjusted[1] * np.cos(delta_theta) + + comps_adjusted[0] * np.sin(delta_theta) ) # 3) perturb along the true radial direction @@ -141,15 +146,20 @@ def points_perturbed_from( ------- The (y,x) coordinates of the input points, which are perturbed by the multipole. """ + symmetry = 360/self.m + k_orig, phi_orig = multipole_k_m_and_phi_m_from(self.specific_multipole_comps, self.m) + comps_adjusted = multipole_comps_from(k_orig, + symmetry-2*phi_orig+(symmetry-(ellipse.angle-phi_orig)), #Re-align light to match mass + self.m) # 1) compute cartesian (polar) angle - theta = np.arctan2(points[:,0], points[:,1]) # <- true polar angle + theta = np.arctan2(points[:, 0], points[:, 1]) # <- true polar angle # 2) multipole in that same frame delta_theta = self.m * (theta - ellipse.angle_radians) radial = ( - self.specific_multipole_comps[1] * np.cos(delta_theta) - + self.specific_multipole_comps[0] * np.sin(delta_theta) + comps_adjusted[1] * np.cos(delta_theta) + + comps_adjusted[0] * np.sin(delta_theta) ) # 3) perturb along the true radial direction From 54a170f181a9c6585128264a755f6db17898a47c Mon Sep 17 00:00:00 2001 From: samlange04 Date: Tue, 14 Oct 2025 21:39:08 +0800 Subject: [PATCH 10/20] Add get_shape_angle function for multipole shapes --- .../ellipse/ellipse/ellipse_multipole.py | 23 ++++++++++++++++++ .../mass/total/power_law_multipole.py | 24 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index d4a47a1c7..15bc652bc 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -38,6 +38,29 @@ class representing the multipole of an ellipse, which is used to perform ellipse self.m = m self.multipole_comps = multipole_comps + def get_shape_angle( + self, + ellipse: Ellipse, + ) -> float: + """ + The shape angle is the offset between the angle of the ellipse and the angle of the multipole, + this defines the shape that the multipole takes. + + In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +/- 45 + indicate pure boxiness. + + Parameters + ---------- + ellipse + The base ellipse profile that is perturbed by the multipole. + + Returns + ------- + The angle between the ellipse and the multipole, in degrees. + """ + + return ellipse.angle-multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] + def points_perturbed_from( self, pixel_scale, points, ellipse: Ellipse, n_i: int = 0 ) -> np.ndarray: diff --git a/autogalaxy/profiles/mass/total/power_law_multipole.py b/autogalaxy/profiles/mass/total/power_law_multipole.py index 663713462..e29ee1991 100644 --- a/autogalaxy/profiles/mass/total/power_law_multipole.py +++ b/autogalaxy/profiles/mass/total/power_law_multipole.py @@ -7,6 +7,7 @@ from autogalaxy import convert from autogalaxy.profiles.mass.abstract.abstract import MassProfile +from autogalaxy.profiles.mass.total import PowerLaw def radial_and_angle_grid_from( @@ -126,6 +127,29 @@ def __init__( ) self.angle_m *= units.deg.to(units.rad) + def get_shape_angle( + self, + base_profile: PowerLaw, + ) -> float: + """ + The shape angle is the offset between the angle of the ellipse and the angle of the multipole, + this defines the shape that the multipole takes. + + In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +/- 45 + indicate pure boxiness. + + Parameters + ---------- + base_profile + The base power-law mass profile that is perturbed by the multipole. + + Returns + ------- + The angle between the ellipse and the multipole, in degrees. + """ + + return convert.angle_from(base_profile.ell_comps) - convert.multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] + def jacobian( self, a_r: np.ndarray, a_angle: np.ndarray, polar_angle_grid: np.ndarray ) -> Tuple[np.ndarray, Tuple]: From 79d10b516a406e5031427ff4b35f82d485634db8 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Tue, 14 Oct 2025 22:38:46 +0800 Subject: [PATCH 11/20] Add get_shape_angle function for multipole shapes --- autogalaxy/ellipse/ellipse/ellipse_multipole.py | 10 ++++++++-- autogalaxy/profiles/mass/total/power_law_multipole.py | 9 ++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index 15bc652bc..760fe4322 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -46,7 +46,7 @@ def get_shape_angle( The shape angle is the offset between the angle of the ellipse and the angle of the multipole, this defines the shape that the multipole takes. - In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +/- 45 + In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +- 45 indicate pure boxiness. Parameters @@ -59,7 +59,13 @@ def get_shape_angle( The angle between the ellipse and the multipole, in degrees. """ - return ellipse.angle-multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] + angle = ellipse.angle-multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] + if angle < -45: + angle += 180 + elif angle > 135: + angle -= 180 + + return angle def points_perturbed_from( self, pixel_scale, points, ellipse: Ellipse, n_i: int = 0 diff --git a/autogalaxy/profiles/mass/total/power_law_multipole.py b/autogalaxy/profiles/mass/total/power_law_multipole.py index e29ee1991..ddd35b054 100644 --- a/autogalaxy/profiles/mass/total/power_law_multipole.py +++ b/autogalaxy/profiles/mass/total/power_law_multipole.py @@ -148,7 +148,14 @@ def get_shape_angle( The angle between the ellipse and the multipole, in degrees. """ - return convert.angle_from(base_profile.ell_comps) - convert.multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] + angle = convert.angle_from(base_profile.ell_comps) - convert.multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] + if angle < -45: + angle += 180 + elif angle > 135: + angle -= 180 + + return angle + def jacobian( self, a_r: np.ndarray, a_angle: np.ndarray, polar_angle_grid: np.ndarray From ea62970b39d39adda33aacd3e8ced5c537495762 Mon Sep 17 00:00:00 2001 From: samlange04 Date: Wed, 15 Oct 2025 14:27:33 +0800 Subject: [PATCH 12/20] Refactor config --- autogalaxy/config/notation.yaml | 4 ++-- autogalaxy/config/priors/ellipse/ellipse_multipole.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/autogalaxy/config/notation.yaml b/autogalaxy/config/notation.yaml index 5c7b64b3e..ba72a6281 100644 --- a/autogalaxy/config/notation.yaml +++ b/autogalaxy/config/notation.yaml @@ -20,8 +20,8 @@ label: ell_comps_1: \epsilon_{\rm 2} multipole_comps_0: M_{\rm 1} multipole_comps_1: M_{\rm 2} - input_multipole_comps_0: M_{\rm 1} - input_multipole_comps_1: M_{\rm 2} + scaled_multipole_comps_0: M_{\rm 1} + scaled_multipole_comps_1: M_{\rm 2} flux: F gamma: \gamma gamma_1: \gamma diff --git a/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml b/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml index 74551d143..8882a7b76 100644 --- a/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml +++ b/autogalaxy/config/priors/ellipse/ellipse_multipole.yaml @@ -21,7 +21,7 @@ EllipseMultipole: upper: inf EllipseMultipoleRelative: - input_multipole_comps_0: + scaled_multipole_comps_0: type: Uniform lower_limit: -0.1 upper_limit: 0.1 @@ -31,7 +31,7 @@ EllipseMultipoleRelative: gaussian_limits: lower: -inf upper: inf - input_multipole_comps_1: + scaled_multipole_comps_1: type: Uniform lower_limit: -0.1 upper_limit: 0.1 From e01db59940472bc12f0e5b9d2f5eec48402a7994 Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:31:28 +0800 Subject: [PATCH 13/20] Update test_multipole.py Update test to match new angle alignment and include EllipseMultipoleScaled --- .../ellipse/ellipse/test_multipole.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test_autogalaxy/ellipse/ellipse/test_multipole.py b/test_autogalaxy/ellipse/ellipse/test_multipole.py index b3d075a11..aa30c9899 100644 --- a/test_autogalaxy/ellipse/ellipse/test_multipole.py +++ b/test_autogalaxy/ellipse/ellipse/test_multipole.py @@ -24,5 +24,20 @@ def test__points_perturbed_from(): pixel_scale=pixel_scale, points=points, ellipse=ellipse ) - assert points_perturbed[1, 0] == pytest.approx(-0.982728, 1.0e-4) + assert points_perturbed[1, 0] == pytest.approx(-0.919384, 1.0e-4) assert points_perturbed[1, 1] == pytest.approx(0.298726, 1.0e-4) + + + ellipse = ag.Ellipse(major_axis=1.5) + + points = ellipse.points_from_major_axis_from(pixel_scale=pixel_scale) + + multipole_scaled = ag.EllipseMultipoleScaled(m=4, scaled_multipole_comps=(0.1, 0.2), major_axis=1.5) + + points_perturbed = multipole_scaled.points_perturbed_from( + pixel_scale=pixel_scale, points=points, ellipse=ellipse + ) + + assert points_perturbed[1, 0] == pytest.approx(-0.848528, 1.0e-4) + assert points_perturbed[1, 1] == pytest.approx(0.848528, 1.0e-4) + From 0ed099ecd621a1d3e5ef5e0c75dc55e595010848 Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:35:13 +0800 Subject: [PATCH 14/20] Update test_fit_ellipse.py Update ellipse multipole to new angle system --- test_autogalaxy/ellipse/test_fit_ellipse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test_autogalaxy/ellipse/test_fit_ellipse.py b/test_autogalaxy/ellipse/test_fit_ellipse.py index 110a4e4a7..c68ef3bf2 100644 --- a/test_autogalaxy/ellipse/test_fit_ellipse.py +++ b/test_autogalaxy/ellipse/test_fit_ellipse.py @@ -63,7 +63,7 @@ def test___points_from_major_axis__multipole(imaging_lh): dataset=imaging_lh, ellipse=ellipse_0, multipole_list=[multipole] ) - assert fit._points_from_major_axis[1, 0] == pytest.approx(-0.542453, 1.0e-4) + assert fit._points_from_major_axis[1, 0] == pytest.approx(-0.119588, 1.0e-4) assert fit._points_from_major_axis[1, 1] == pytest.approx(-0.038278334, 1.0e-4) @@ -197,3 +197,4 @@ 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.169821080058, 1.0e-4) + From fe618940028d9d7b72ef52f67e4ebe2e80559c09 Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:46:42 +0800 Subject: [PATCH 15/20] Update test_fit_ellipse.py --- test_autogalaxy/ellipse/test_fit_ellipse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test_autogalaxy/ellipse/test_fit_ellipse.py b/test_autogalaxy/ellipse/test_fit_ellipse.py index c68ef3bf2..245b64b2f 100644 --- a/test_autogalaxy/ellipse/test_fit_ellipse.py +++ b/test_autogalaxy/ellipse/test_fit_ellipse.py @@ -64,7 +64,7 @@ def test___points_from_major_axis__multipole(imaging_lh): ) assert fit._points_from_major_axis[1, 0] == pytest.approx(-0.119588, 1.0e-4) - assert fit._points_from_major_axis[1, 1] == pytest.approx(-0.038278334, 1.0e-4) + assert fit._points_from_major_axis[1, 1] == pytest.approx(0.038856679, 1.0e-4) # def test__mask_interp(imaging_lh, imaging_lh_masked): @@ -198,3 +198,4 @@ def test__log_likelihood(imaging_lh, imaging_lh_masked): assert fit.log_likelihood == pytest.approx(-0.169821080058, 1.0e-4) + From 6a15a87e1efa5f86ec896ea1aaa72ec6545b224e Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:20:28 +0800 Subject: [PATCH 16/20] Update ellipse_multipole.py --- autogalaxy/ellipse/ellipse/ellipse_multipole.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index 760fe4322..b6b3d346b 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -60,9 +60,9 @@ def get_shape_angle( """ angle = ellipse.angle-multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] - if angle < -45: + if angle < -90: angle += 180 - elif angle > 135: + elif angle > 90: angle -= 180 return angle @@ -196,3 +196,4 @@ def points_perturbed_from( y = points[:, 0] + radial * np.sin(theta) return np.stack((y, x), axis=-1) + From ac9274c7d9a08d6b94162adc7aaf2e5d91bcdd0b Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:21:52 +0800 Subject: [PATCH 17/20] Update shape angle to pm90deg --- autogalaxy/profiles/mass/total/power_law_multipole.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/autogalaxy/profiles/mass/total/power_law_multipole.py b/autogalaxy/profiles/mass/total/power_law_multipole.py index 66d66958a..c86ca1caa 100644 --- a/autogalaxy/profiles/mass/total/power_law_multipole.py +++ b/autogalaxy/profiles/mass/total/power_law_multipole.py @@ -137,7 +137,7 @@ def get_shape_angle( The shape angle is the offset between the angle of the ellipse and the angle of the multipole, this defines the shape that the multipole takes. - In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +/- 45 + In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +- 45 indicate pure boxiness. Parameters @@ -147,13 +147,13 @@ def get_shape_angle( Returns ------- - The angle between the ellipse and the multipole, in degrees. + The angle between the ellipse and the multipole, in degrees, between +-90. """ angle = convert.angle_from(base_profile.ell_comps) - convert.multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] - if angle < -45: + if angle <= -90: angle += 180 - elif angle > 135: + elif angle > 90: angle -= 180 return angle @@ -257,3 +257,4 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, **kwargs) -> np.ndarray: The grid of (y,x) arc-second coordinates the deflection angles are computed on. """ return jnp.zeros(shape=grid.shape[0]) + From a9de7fd70c48c1671017686e3e4fedf5082c1c6d Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:22:37 +0800 Subject: [PATCH 18/20] Update shape angle to pm90deg --- autogalaxy/ellipse/ellipse/ellipse_multipole.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index b6b3d346b..d7e8f4da4 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -56,11 +56,11 @@ def get_shape_angle( Returns ------- - The angle between the ellipse and the multipole, in degrees. + The angle between the ellipse and the multipole, in degrees between +-90. """ angle = ellipse.angle-multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] - if angle < -90: + if angle <= -90: angle += 180 elif angle > 90: angle -= 180 @@ -197,3 +197,4 @@ def points_perturbed_from( return np.stack((y, x), axis=-1) + From e8a982e20fde81c23c44b74bd4fd6e21749d07ff Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:31:09 +0800 Subject: [PATCH 19/20] Fix shape angle ranges --- autogalaxy/ellipse/ellipse/ellipse_multipole.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/autogalaxy/ellipse/ellipse/ellipse_multipole.py b/autogalaxy/ellipse/ellipse/ellipse_multipole.py index d7e8f4da4..f9cfbb690 100644 --- a/autogalaxy/ellipse/ellipse/ellipse_multipole.py +++ b/autogalaxy/ellipse/ellipse/ellipse_multipole.py @@ -46,7 +46,7 @@ def get_shape_angle( The shape angle is the offset between the angle of the ellipse and the angle of the multipole, this defines the shape that the multipole takes. - In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +- 45 + In the case of the m=4 multipole, angles of 0 indicate pure diskiness, angles +- 45 indicate pure boxiness. Parameters @@ -56,14 +56,14 @@ def get_shape_angle( Returns ------- - The angle between the ellipse and the multipole, in degrees between +-90. + The angle between the ellipse and the multipole, in degrees between +- 180/m. """ angle = ellipse.angle-multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] - if angle <= -90: - angle += 180 - elif angle > 90: - angle -= 180 + if angle < -180/self.m: + angle += 360/self.m + elif angle > 180/self.m: + angle -= 360/self.m return angle @@ -198,3 +198,4 @@ def points_perturbed_from( return np.stack((y, x), axis=-1) + From 4d91bc9e38f79002f5619606b60f022082a1215c Mon Sep 17 00:00:00 2001 From: samlange04 <80762641+samlange04@users.noreply.github.com> Date: Sat, 1 Nov 2025 21:31:58 +0800 Subject: [PATCH 20/20] Fix shape angle ranges --- .../profiles/mass/total/power_law_multipole.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/autogalaxy/profiles/mass/total/power_law_multipole.py b/autogalaxy/profiles/mass/total/power_law_multipole.py index c86ca1caa..780fe0b44 100644 --- a/autogalaxy/profiles/mass/total/power_law_multipole.py +++ b/autogalaxy/profiles/mass/total/power_law_multipole.py @@ -137,7 +137,7 @@ def get_shape_angle( The shape angle is the offset between the angle of the ellipse and the angle of the multipole, this defines the shape that the multipole takes. - In the case of the m=4 multipole, angles of 0 or 90 indicate pure diskiness, angles +- 45 + In the case of the m=4 multipole, angles of 0 indicate pure diskiness, angles +- 45 indicate pure boxiness. Parameters @@ -147,14 +147,14 @@ def get_shape_angle( Returns ------- - The angle between the ellipse and the multipole, in degrees, between +-90. + The angle between the ellipse and the multipole, in degrees, between +- 180/m. """ angle = convert.angle_from(base_profile.ell_comps) - convert.multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1] - if angle <= -90: - angle += 180 - elif angle > 90: - angle -= 180 + if angle < -180/self.m: + angle += 360/self.m + elif angle > 180/self.m: + angle -= 360/self.m return angle @@ -258,3 +258,4 @@ def potential_2d_from(self, grid: aa.type.Grid2DLike, **kwargs) -> np.ndarray: """ return jnp.zeros(shape=grid.shape[0]) +