Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f3d748c
Update subplot_fit_ellipse to use log_10 for data, and remove Fill op…
samlange04 Jul 30, 2025
b030cf8
Add EllipseMultipoleRelative class
samlange04 Jul 30, 2025
371d1c4
Add EllipseMultipoleRelative config
samlange04 Jul 30, 2025
50fcfef
Fix declaration of input_multipole_comps
samlange04 Jul 30, 2025
4f7f40a
fixed multipole code
Aug 7, 2025
59d9280
mix in with Sams code
Aug 7, 2025
8399338
Refactor EllipseMultipoleRelative to EllipseMultipoleScaled and add d…
samlange04 Aug 8, 2025
f9d47ab
Add option to disable the data contours in a fit ellipse subplot
samlange04 Aug 8, 2025
5f68c04
fixes
samlange04 Aug 13, 2025
9677b2e
Add fixes to align ellipse multipole angle behaviour to EPL multipole…
samlange04 Oct 13, 2025
54a170f
Add get_shape_angle function for multipole shapes
samlange04 Oct 14, 2025
79d10b5
Add get_shape_angle function for multipole shapes
samlange04 Oct 14, 2025
ea62970
Refactor config
samlange04 Oct 15, 2025
834b528
Merge branch 'main' into feature/ellipse_for_sam
samlange04 Oct 15, 2025
e01db59
Update test_multipole.py
samlange04 Oct 16, 2025
0ed099e
Update test_fit_ellipse.py
samlange04 Oct 16, 2025
fe61894
Update test_fit_ellipse.py
samlange04 Oct 16, 2025
6a15a87
Update ellipse_multipole.py
samlange04 Oct 16, 2025
ac9274c
Update shape angle to pm90deg
samlange04 Oct 16, 2025
a9de7fd
Update shape angle to pm90deg
samlange04 Oct 16, 2025
a7bba07
Merge branch 'main' into feature/ellipse_for_sam
samlange04 Oct 27, 2025
e8a982e
Fix shape angle ranges
samlange04 Nov 1, 2025
4d91bc9
Fix shape angle ranges
samlange04 Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions autogalaxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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 EllipseMultipoleScaled
from .ellipse.fit_ellipse import FitEllipse
from .ellipse.model.analysis import AnalysisEllipse
from .operate.image import OperateImage
Expand Down
4 changes: 4 additions & 0 deletions autogalaxy/config/notation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ label:
ell_comps_1: \epsilon_{\rm 2}
multipole_comps_0: M_{\rm 1}
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
Expand Down Expand Up @@ -94,6 +96,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}'
Expand Down
22 changes: 22 additions & 0 deletions autogalaxy/config/priors/ellipse/ellipse_multipole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,25 @@ EllipseMultipole:
limits:
lower: -inf
upper: inf

EllipseMultipoleRelative:
scaled_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
scaled_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
2 changes: 1 addition & 1 deletion autogalaxy/ellipse/ellipse/ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
147 changes: 139 additions & 8 deletions autogalaxy/ellipse/ellipse/ellipse_multipole.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,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.
Expand All @@ -37,6 +38,35 @@ class representing the multipole of an ellispe with, which is used to perform el
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 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 between +- 180/m.
"""

angle = ellipse.angle-multipole_k_m_and_phi_m_from(self.multipole_comps, self.m)[1]
if angle < -180/self.m:
angle += 360/self.m
elif angle > 180/self.m:
angle -= 360/self.m

return angle

def points_perturbed_from(
self, pixel_scale, points, ellipse: Ellipse, n_i: int = 0
) -> np.ndarray:
Expand All @@ -56,15 +86,116 @@ 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

# 2) multipole in that same frame
delta_theta = self.m * (theta - ellipse.angle_radians)
radial = (
comps_adjusted[1] * np.cos(delta_theta)
+ comps_adjusted[0] * np.sin(delta_theta)
)

angles = ellipse.angles_from_x0_from(pixel_scale=pixel_scale, n_i=n_i)
# 3) perturb along the true radial direction
x = points[:, 1] + radial * np.cos(theta)
y = points[:, 0] + radial * np.sin(theta)

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)),
return np.stack((y, x), axis=-1)

class EllipseMultipoleScaled(EllipseMultipole):
def __init__(
self,
m=4,
scaled_multipole_comps: Tuple[float, float] = (0.0, 0.0),
major_axis=1.,
):
"""
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

specific_multipole_comps = multipole_comps_from(k_adjusted, phi, m)

super().__init__(m, specific_multipole_comps)

self.specific_multipole_comps = specific_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.
"""
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

# 2) multipole in that same frame
delta_theta = self.m * (theta - ellipse.angle_radians)
radial = (
comps_adjusted[1] * np.cos(delta_theta)
+ comps_adjusted[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((y, x), axis=-1)



return np.stack(arrays=(y, x), axis=-1)
2 changes: 1 addition & 1 deletion autogalaxy/ellipse/fit_ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions autogalaxy/ellipse/plot/fit_ellipse_plotters.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,15 @@ 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.
"""

self.open_subplot_figure(number_subplots=2)

self.figures_2d(data=True)
self.mat_plot_2d.use_log10 = 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")
Expand Down Expand Up @@ -227,7 +228,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",
),
)

Expand Down
2 changes: 0 additions & 2 deletions autogalaxy/plot/mat_plot/two_d.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -197,7 +196,6 @@ def __init__(
xlabel=xlabel,
text=text,
annotate=annotate,
fill=fill,
output=output,
origin_scatter=origin_scatter,
mask_scatter=mask_scatter,
Expand Down
Loading
Loading