Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions autogalaxy/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
55 changes: 28 additions & 27 deletions autogalaxy/ellipse/ellipse/ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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.

Expand All @@ -126,16 +126,19 @@ 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
-------
The angles from the x-axis to the points on the circle.
"""
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) -> 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.
Expand All @@ -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, n_i=n_i)

return np.divide(
self.major_axis * self.minor_axis,
Expand All @@ -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, 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.
Expand All @@ -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, n_i=n_i)
ellipse_radii_from_major_axis = self.ellipse_radii_from_major_axis_from(
pixel_scale=pixel_scale
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
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
Expand All @@ -200,37 +209,29 @@ 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.

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, n_i=n_i)
ellipse_radii_from_major_axis = self.ellipse_radii_from_major_axis_from(
pixel_scale=pixel_scale
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
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
Expand All @@ -239,21 +240,21 @@ 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
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, 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:
Expand Down
132 changes: 80 additions & 52 deletions autogalaxy/ellipse/fit_ellipse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -48,17 +48,45 @@ 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:

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],
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(
Expand All @@ -80,51 +108,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:
Expand All @@ -146,7 +174,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)

Expand All @@ -170,7 +198,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)

Expand Down
10 changes: 7 additions & 3 deletions autogalaxy/ellipse/model/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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]:
"""
Expand Down
6 changes: 5 additions & 1 deletion autogalaxy/ellipse/model/plotter_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,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(
Expand Down
Loading
Loading