diff --git a/autolens/point/analysis.py b/autolens/point/analysis.py index 256fbbaf1..1c3553ca7 100644 --- a/autolens/point/analysis.py +++ b/autolens/point/analysis.py @@ -147,7 +147,7 @@ def _log_likelihood_for_coordinates( "The number of predicted coordinates must be equal to the number of observed coordinates." ) - predicted_coordinates = set(predicted_coordinates) + predicted_coordinates = set(map(tuple, predicted_coordinates)) observed_coordinates = set(self.observed_coordinates) log_likelihood = 0.0 diff --git a/autolens/point/triangles/triangle_solver.py b/autolens/point/triangles/triangle_solver.py index d9b3887d7..d869fa22f 100644 --- a/autolens/point/triangles/triangle_solver.py +++ b/autolens/point/triangles/triangle_solver.py @@ -1,17 +1,42 @@ import logging import math -from typing import Tuple, List +from dataclasses import dataclass +from typing import Tuple, List, Iterator from autoarray import Grid2D, Grid2DIrregular -from autoarray.structures.triangles.subsample_triangles import SubsampleTriangles -from autoarray.structures.triangles.triangles import Triangles +from autoarray.structures.triangles.array import ArrayTriangles from autoarray.type import Grid2DLike from autogalaxy import OperateDeflections - logger = logging.getLogger(__name__) +@dataclass +class Step: + """ + A step in the triangle solver algorithm. + + Attributes + ---------- + number + The number of the step. + initial_triangles + The triangles at the start of the step. + filtered_triangles + The triangles trace to triangles that contain the source plane coordinate. + neighbourhood + The neighbourhood of the filtered triangles. + up_sampled + The neighbourhood up-sampled to increase the resolution. + """ + + number: int + initial_triangles: ArrayTriangles + filtered_triangles: ArrayTriangles + neighbourhood: ArrayTriangles + up_sampled: ArrayTriangles + + class TriangleSolver: def __init__( self, @@ -87,31 +112,18 @@ def solve( ------- A list of image plane coordinates that are traced to the source plane coordinate. """ - triangles = Triangles.for_grid(grid=self.grid) - if self.n_steps == 0: raise ValueError( "The target pixel scale is too large to subdivide the triangles." ) - kept_triangles = [] - - for _ in range(self.n_steps): - kept_triangles = self._filter_triangles( - triangles=triangles, - source_plane_coordinate=source_plane_coordinate, - ) - with_neighbourhood = { - triangle - for kept_triangle in kept_triangles - for triangle in kept_triangle.neighbourhood - } - triangles = SubsampleTriangles(parent_triangles=list(with_neighbourhood)) + steps = list(self.steps(source_plane_coordinate=source_plane_coordinate)) + final_step = steps[-1] + kept_triangles = final_step.filtered_triangles - means = [triangle.mean for triangle in kept_triangles] - filtered_means = self._filter_low_magnification(points=means) + filtered_means = self._filter_low_magnification(points=kept_triangles.means) - difference = len(means) - len(filtered_means) + difference = len(kept_triangles.means) - len(filtered_means) if difference > 0: logger.debug( f"Filtered one multiple-image with magnification below threshold." @@ -152,7 +164,7 @@ def _filter_low_magnification( def _filter_triangles( self, - triangles: Triangles, + triangles: ArrayTriangles, source_plane_coordinate: Tuple[float, float], ): """ @@ -169,16 +181,45 @@ def _filter_triangles( ------- The triangles that contain the source plane coordinate. """ - source_plane_grid = self._source_plane_grid(grid=triangles.grid_2d) - - kept_triangles = [] - for image_triangle, source_triangle in zip( - triangles.triangles, - triangles.with_updated_grid(source_plane_grid), - ): - if source_triangle.contains( - point=source_plane_coordinate, - ): - kept_triangles.append(image_triangle) - - return kept_triangles + source_plane_grid = self._source_plane_grid( + grid=Grid2DIrregular(triangles.vertices) + ) + source_triangles = triangles.with_vertices(source_plane_grid) + indexes = source_triangles.containing_indices(point=source_plane_coordinate) + return triangles.for_indexes(indexes=indexes) + + def steps( + self, + source_plane_coordinate: Tuple[float, float], + ) -> Iterator[Step]: + """ + Iterate over the steps of the triangle solver algorithm. + + Parameters + ---------- + source_plane_coordinate + The source plane coordinate to trace to the image plane. + + Returns + ------- + An iterator over the steps of the triangle solver algorithm. + """ + initial_triangles = ArrayTriangles.for_grid(grid=self.grid) + + for number in range(self.n_steps): + kept_triangles = self._filter_triangles( + initial_triangles, + source_plane_coordinate, + ) + neighbourhood = kept_triangles.neighborhood() + up_sampled = neighbourhood.up_sample() + + yield Step( + number=number, + initial_triangles=initial_triangles, + filtered_triangles=kept_triangles, + neighbourhood=neighbourhood, + up_sampled=up_sampled, + ) + + initial_triangles = up_sampled diff --git a/autolens/point/triangles/visualise.py b/autolens/point/triangles/visualise.py new file mode 100644 index 000000000..87b7fabea --- /dev/null +++ b/autolens/point/triangles/visualise.py @@ -0,0 +1,23 @@ +from .triangle_solver import Step +from matplotlib import pyplot as plt +import numpy as np + + +def add_triangles(triangles, color): + for triangle in triangles: + triangle = np.append(triangle, [triangle[0]], axis=0) + plt.plot(triangle[:, 0], triangle[:, 1], "o-", color=color) + + +def visualise(step: Step): + plt.figure(figsize=(8, 8)) + add_triangles(step.initial_triangles, color="black") + add_triangles(step.filtered_triangles, color="blue") + add_triangles(step.up_sampled, color="green") + add_triangles(step.neighbourhood, color="red") + + plt.xlabel("X") + plt.ylabel("Y") + plt.title(f"Step {step.number}") + plt.gca().set_aspect("equal", adjustable="box") + plt.show() diff --git a/test_autolens/point/triangles/conftest.py b/test_autolens/point/triangles/conftest.py index 321efc191..bafeafe95 100644 --- a/test_autolens/point/triangles/conftest.py +++ b/test_autolens/point/triangles/conftest.py @@ -5,6 +5,6 @@ @pytest.fixture def grid(): return al.Grid2D.uniform( - shape_native=(100, 100), - pixel_scales=0.05, + shape_native=(10, 10), + pixel_scales=1.0, ) diff --git a/test_autolens/point/triangles/test_solver.py b/test_autolens/point/triangles/test_solver.py index 55032fcbe..201a4594e 100644 --- a/test_autolens/point/triangles/test_solver.py +++ b/test_autolens/point/triangles/test_solver.py @@ -36,7 +36,7 @@ def test_solver(solver): def test_steps(solver): - assert solver.n_steps == 3 + assert solver.n_steps == 7 class NullTracer(al.Tracer): @@ -52,6 +52,7 @@ def deflections_yx_2d_from(self, grid): [ (0.0, 0.0), (0.0, 1.0), + (1.0, 0.0), (1.0, 1.0), (0.5, 0.5), (0.1, 0.1), @@ -70,7 +71,7 @@ def test_trivial( (coordinates,) = solver.solve( source_plane_coordinate=source_plane_coordinate, ) - assert coordinates == pytest.approx(source_plane_coordinate, abs=1.0e-2) + assert coordinates == pytest.approx(source_plane_coordinate, abs=1.0e-1) def test_real_example(grid): @@ -97,4 +98,4 @@ def test_real_example(grid): pixel_scale_precision=0.001, ) result = solver.solve((0.07, 0.07)) - assert len(result) == 4 + assert len(result) == 5