diff --git a/autolens/config/visualize/plots.yaml b/autolens/config/visualize/plots.yaml index 19959d32a..d1c85c0ea 100644 --- a/autolens/config/visualize/plots.yaml +++ b/autolens/config/visualize/plots.yaml @@ -7,6 +7,10 @@ psf: false positions: # Settings for plots with resampling image-positions on (e.g. the image). image_with_positions: true + point_dataset: # Settings for plots of point source datasets (e.g. PointDatasetPlotter). + subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the data, noise-map, etc.)? + positions: false # Plot the positions of th multiple images of the point source in the image plane? + fluxes: false # Plot the fluxes of the multiple images of the point source in the image plane? fit: # Settings for plots of all fits (e.g. FitImagingPlotter, FitInterferometerPlotter). subplot_fit: true # Plot subplot of all fit quantities for any dataset (e.g. the model data, residual-map, etc.)? all_at_end_png: true # Plot all individual plots listed below as .png (even if False)? @@ -25,6 +29,9 @@ subtracted_images_of_planes: false # Plot individual plots of each plane's subtracted image? plane_images_of_planes: false # Plot individual plots of each plane's image (e.g. in the source plane)? fit_imaging: {} # Settings for plots of fits to imaging datasets (e.g. FitImagingPlotter). + fit_point_dataset: # Settings for plots of fits to point source datasets (e.g. FitPointDatasetPlotter). + positions: false # Plot the positions of th multiple images of the point source in the image plane? + fluxes: false # Plot the fluxes of the multiple images of the point source in the image plane? tracer: # Settings for plots of tracers (e.g. TracerPlotter). subplot_tracer: true # Plot subplot of all quantities in each tracer (e.g. images, convergence)? all_at_end_png: true # Plot all individual plots listed below as .png (even if False)? diff --git a/autolens/point/dataset.py b/autolens/point/dataset.py index f4aa76f85..3214e70f7 100644 --- a/autolens/point/dataset.py +++ b/autolens/point/dataset.py @@ -83,3 +83,12 @@ def info(self) -> str: info += f"fluxes : {self.fluxes}\n" info += f"fluxes_noise_map : {self.fluxes_noise_map}\n" return info + + def extent_from(self, buffer : float = 0.1): + + y_max = max(self.positions[:, 0]) + buffer + y_min = min(self.positions[:, 0]) - buffer + x_max = max(self.positions[:, 1]) + buffer + x_min = min(self.positions[:, 1]) - buffer + + return [y_min, y_max, x_min, x_max] \ No newline at end of file diff --git a/autolens/point/model/analysis.py b/autolens/point/model/analysis.py index f41db7a75..3dd23e6e1 100644 --- a/autolens/point/model/analysis.py +++ b/autolens/point/model/analysis.py @@ -6,11 +6,11 @@ from autogalaxy.analysis.analysis.analysis import Analysis as AgAnalysis from autolens.analysis.analysis.lens import AnalysisLens -from autolens.analysis.plotter_interface import PlotterInterface from autolens.point.fit.positions.image.pair_repeat import FitPositionsImagePairRepeat from autolens.point.fit.dataset import FitPointDataset from autolens.point.dataset import PointDataset from autolens.point.model.result import ResultPoint +from autolens.point.model.visualizer import VisualizerPoint from autolens.point.solver import PointSolver from autolens import exc @@ -24,6 +24,8 @@ class AnalysisPoint(AgAnalysis, AnalysisLens): + + Visualizer = VisualizerPoint Result = ResultPoint def __init__( @@ -104,11 +106,6 @@ def fit_from( run_time_dict=run_time_dict, ) - def visualize(self, paths, instance, during_analysis): - tracer = self.tracer_via_instance_from(instance=instance) - - plotter_interface = PlotterInterface(image_path=paths.image_path) - def save_attributes(self, paths: af.DirectoryPaths): ag.output_to_json( obj=self.dataset, diff --git a/autolens/point/model/plotter_interface.py b/autolens/point/model/plotter_interface.py new file mode 100644 index 000000000..a1962fd83 --- /dev/null +++ b/autolens/point/model/plotter_interface.py @@ -0,0 +1,119 @@ +from os import path + +from autolens.analysis.plotter_interface import PlotterInterface + +from autolens.point.fit.dataset import FitPointDataset +from autolens.point.plot.fit_point_plotters import FitPointDatasetPlotter +from autolens.point.dataset import PointDataset +from autolens.point.plot.point_dataset_plotters import PointDatasetPlotter + +from autolens.analysis.plotter_interface import plot_setting + + +class PlotterInterfacePoint(PlotterInterface): + + def dataset_point(self, dataset: PointDataset): + """ + Output visualization of an `PointDataset` dataset, typically before a model-fit is performed. + + Images are output to the `image` folder of the `image_path` in a subfolder called `dataset`. When used with + a non-linear search the `image_path` is the output folder of the non-linear search. + `. + Visualization includes individual images of the different points of the dataset (e.g. the positions and fluxes) + + The images output by the `PlotterInterface` are customized using the file `config/visualize/plots.yaml` under + the `dataset` header. + + Parameters + ---------- + dataset + The imaging dataset which is visualized. + """ + + def should_plot(name): + return plot_setting(section=["point_dataset"], name=name) + + mat_plot_2d = self.mat_plot_2d_from(subfolders="dataset") + + dataset_plotter = PointDatasetPlotter( + dataset=dataset, mat_plot_2d=mat_plot_2d, include_2d=self.include_2d + ) + + dataset_plotter.figures_2d( + positions=should_plot("positions"), + fluxes=should_plot("fluxes"), + ) + + mat_plot_2d = self.mat_plot_2d_from(subfolders="") + + dataset_plotter = PointDatasetPlotter( + dataset=dataset, mat_plot_2d=mat_plot_2d, include_2d=self.include_2d + ) + + if should_plot("subplot_dataset"): + dataset_plotter.subplot_dataset() + + def fit_point( + self, fit: FitPointDataset, during_analysis: bool, subfolders: str = "fit_dataset" + ): + """ + Visualizes a `FitPointDataset` object, which fits an imaging dataset. + + Images are output to the `image` folder of the `image_path` in a subfolder called `fit`. When + used with a non-linear search the `image_path` points to the search's results folder and this function + visualizes the maximum log likelihood `FitImaging` inferred by the search so far. + + Visualization includes individual images of attributes of the `FitPointDataset` (e.g. the model data and data) + and a subplot of all `FitPointDataset`'s images on the same figure. + + The images output by the `PlotterInterface` are customized using the file `config/visualize/plots.yaml` under the + [fit] header. + + Parameters + ---------- + fit + The maximum log likelihood `FitPointDataset` of the non-linear search which is used to plot the fit. + during_analysis + Whether visualization is performed during a non-linear search or once it is completed. + visuals_2d + An object containing attributes which may be plotted over the figure (e.g. the centres of mass and light + profiles). + """ + + def should_plot(name): + return plot_setting(section=["fit", "fit_point_dataset"], name=name) + + mat_plot_2d = self.mat_plot_2d_from(subfolders=subfolders) + + fit_plotter = FitPointDatasetPlotter( + fit=fit, mat_plot_2d=mat_plot_2d, include_2d=self.include_2d + ) + + fit_plotter.figures_2d( + positions=should_plot("positions"), + fluxes=should_plot("fluxes"), + ) + + mat_plot_2d = self.mat_plot_2d_from(subfolders="") + + fit_plotter = FitPointDatasetPlotter( + fit=fit, mat_plot_2d=mat_plot_2d, include_2d=self.include_2d + ) + + if should_plot("subplot_fit"): + fit_plotter.subplot_fit() + + if not during_analysis and should_plot("all_at_end_png"): + + mat_plot_2d = self.mat_plot_2d_from( + subfolders=path.join("fit_dataset", "end"), + ) + + fit_plotter = FitPointDatasetPlotter( + fit=fit, mat_plot_2d=mat_plot_2d, include_2d=self.include_2d + ) + + fit_plotter.figures_2d( + positions=True, + fluxes=True + ) diff --git a/autolens/point/model/visualizer.py b/autolens/point/model/visualizer.py new file mode 100644 index 000000000..1cec8e660 --- /dev/null +++ b/autolens/point/model/visualizer.py @@ -0,0 +1,88 @@ +import autofit as af +import autogalaxy as ag + +from autolens.point.model.plotter_interface import PlotterInterfacePoint + + +class VisualizerPoint(af.Visualizer): + + @staticmethod + def visualize_before_fit( + analysis, + paths: af.AbstractPaths, + model: af.AbstractPriorModel, + ): + """ + PyAutoFit calls this function immediately before the non-linear search begins. + + It visualizes objects which do not change throughout the model fit like the dataset. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization and the pickled objects used by the aggregator output by this function. + model + The model object, which includes model components representing the galaxies that are fitted to + the imaging data. + """ + + plotter_interface = PlotterInterfacePoint( + image_path=paths.image_path, title_prefix=analysis.title_prefix + ) + + plotter_interface.dataset_point(dataset=analysis.dataset) + + @staticmethod + def visualize( + analysis, + paths: af.DirectoryPaths, + instance: af.ModelInstance, + during_analysis: bool, + ): + """ + Output images of the maximum log likelihood model inferred by the model-fit. This function is called throughout + the non-linear search at regular intervals, and therefore provides on-the-fly visualization of how well the + model-fit is going. + + The visualization performed by this function includes: + + - Images of the best-fit `Tracer`, including the images of each of its galaxies. + + - Images of the best-fit `FitPointDataset`, including the model-image, residuals and chi-squared of its fit to + the imaging data. + + The images output by this function are customized using the file `config/visualize/plots.yaml`. + + Parameters + ---------- + paths + The paths object which manages all paths, e.g. where the non-linear search outputs are stored, + visualization, and the pickled objects used by the aggregator output by this function. + instance + An instance of the model that is being fitted to the data by this analysis (whose parameters have been set + via a non-linear search). + during_analysis + If True the visualization is being performed midway through the non-linear search before it is finished, + which may change which images are output. + """ + fit = analysis.fit_from(instance=instance) + + plotter_interface = PlotterInterfacePoint( + image_path=paths.image_path, title_prefix=analysis.title_prefix + ) + + plotter_interface.fit_point(fit=fit, during_analysis=during_analysis) + + tracer = fit.tracer + + grid = ag.Grid2D.from_extent(extent=fit.dataset.extent_from(), shape_native=(100, 100)) + + plotter_interface.tracer( + tracer=tracer, grid=grid, during_analysis=during_analysis + ) + plotter_interface.galaxies( + galaxies=tracer.galaxies, + grid=fit.grids.uniform, + during_analysis=during_analysis, + ) diff --git a/autolens/point/plot/fit_point_plotters.py b/autolens/point/plot/fit_point_plotters.py index c70a49fb5..16eb8b220 100644 --- a/autolens/point/plot/fit_point_plotters.py +++ b/autolens/point/plot/fit_point_plotters.py @@ -1,11 +1,10 @@ -from autoarray.plot.abstract_plotters import AbstractPlotter - import autogalaxy.plot as aplt +from autolens.plot.abstract_plotters import Plotter from autolens.point.fit.dataset import FitPointDataset -class FitPointDatasetPlotter(AbstractPlotter): +class FitPointDatasetPlotter(Plotter): def __init__( self, fit: FitPointDataset, @@ -37,7 +36,34 @@ def figures_2d(self, positions: bool = False, fluxes: bool = False): if positions: visuals_2d = self.get_visuals_2d() - visuals_2d += visuals_2d.__class__(positions=self.fit.positions.model_data) + visuals_2d += visuals_2d.__class__( + multiple_images=self.fit.positions.model_data + ) + + if self.mat_plot_2d.axis.kwargs.get("extent") is None: + + buffer = 0.1 + + y_max = max( + max(self.fit.dataset.positions[:, 0]), + max(self.fit.positions.model_data[:, 0]), + ) + buffer + y_min = min( + min(self.fit.dataset.positions[:, 0]), + min(self.fit.positions.model_data[:, 0]), + ) - buffer + x_max = max( + max(self.fit.dataset.positions[:, 1]), + max(self.fit.positions.model_data[:, 1]), + ) + buffer + x_min = min( + min(self.fit.dataset.positions[:, 1]), + min(self.fit.positions.model_data[:, 1]), + ) - buffer + + extent = [y_min, y_max, x_min, x_max] + + self.mat_plot_2d.axis.kwargs["extent"] = extent self.mat_plot_2d.plot_grid( grid=self.fit.dataset.positions, @@ -46,7 +72,7 @@ def figures_2d(self, positions: bool = False, fluxes: bool = False): visuals_2d=visuals_2d, auto_labels=aplt.AutoLabels( title=f"{self.fit.dataset.name} Fit Positions", - filename="fit_point_dataset_positions", + filename="fit_point_positions", ), buffer=0.1, ) @@ -76,7 +102,7 @@ def figures_2d(self, positions: bool = False, fluxes: bool = False): visuals_1d=visuals_1d, auto_labels=aplt.AutoLabels( title=f" {self.fit.dataset.name} Fit Fluxes", - filename="fit_point_dataset_fluxes", + filename="fit_point_fluxes", xlabel="Point Number", ), plot_axis_type_override="errorbar", diff --git a/test_autolens/config/visualize.yaml b/test_autolens/config/visualize.yaml index 9a249a24f..f4d4bd3b9 100644 --- a/test_autolens/config/visualize.yaml +++ b/test_autolens/config/visualize.yaml @@ -104,6 +104,9 @@ plots: dirty_signal_to_noise_map: false phases_vs_uv_distances: false uv_wavelengths: false + fit_point_dataset: # Settings for plots of fits to point source datasets (e.g. FitPointDatasetPlotter). + positions: true # Plot the positions of th multiple images of the point source in the image plane? + fluxes: true # Plot the fluxes of the multiple images of the point source in the image plane? fit_quantity: all_at_end_fits: true all_at_end_png: true diff --git a/test_autolens/point/model/test_plotter_interface_point.py b/test_autolens/point/model/test_plotter_interface_point.py new file mode 100644 index 000000000..f018daf32 --- /dev/null +++ b/test_autolens/point/model/test_plotter_interface_point.py @@ -0,0 +1,32 @@ +import os +import shutil +from os import path + +import pytest +from autolens.point.model.plotter_interface import PlotterInterfacePoint + +directory = path.dirname(path.realpath(__file__)) + + +@pytest.fixture(name="plot_path") +def make_plotter_interface_plotter_setup(): + return path.join("{}".format(directory), "files") + + +def test__fit_point( + fit_point_dataset_x2_plane, include_2d_all, plot_path, plot_patch +): + if os.path.exists(plot_path): + shutil.rmtree(plot_path) + + plotter_interface = PlotterInterfacePoint(image_path=plot_path) + + plotter_interface.fit_point( + fit=fit_point_dataset_x2_plane, during_analysis=False + ) + + assert path.join(plot_path, "subplot_fit.png") in plot_patch.paths + + plot_path = path.join(plot_path, "fit_dataset") + + assert path.join(plot_path, "fit_point_positions.png") in plot_patch.paths \ No newline at end of file diff --git a/test_autolens/point/plot/test_fit_point_plotters.py b/test_autolens/point/plot/test_fit_point_plotters.py index 278fe9b8f..d544e8de7 100644 --- a/test_autolens/point/plot/test_fit_point_plotters.py +++ b/test_autolens/point/plot/test_fit_point_plotters.py @@ -29,15 +29,15 @@ def test__fit_point_quantities_are_output( fit_point_plotter.figures_2d(positions=True, fluxes=True) - assert path.join(plot_path, "fit_point_dataset_positions.png") in plot_patch.paths - assert path.join(plot_path, "fit_point_dataset_fluxes.png") in plot_patch.paths + assert path.join(plot_path, "fit_point_positions.png") in plot_patch.paths + assert path.join(plot_path, "fit_point_fluxes.png") in plot_patch.paths plot_patch.paths = [] fit_point_plotter.figures_2d(positions=True, fluxes=False) - assert path.join(plot_path, "fit_point_dataset_positions.png") in plot_patch.paths - assert path.join(plot_path, "fit_point_dataset_fluxes.png") not in plot_patch.paths + assert path.join(plot_path, "fit_point_positions.png") in plot_patch.paths + assert path.join(plot_path, "fit_point_fluxes.png") not in plot_patch.paths plot_patch.paths = [] @@ -51,8 +51,8 @@ def test__fit_point_quantities_are_output( fit_point_plotter.figures_2d(positions=True, fluxes=True) - assert path.join(plot_path, "fit_point_dataset_positions.png") in plot_patch.paths - assert path.join(plot_path, "fit_point_dataset_fluxes.png") not in plot_patch.paths + assert path.join(plot_path, "fit_point_positions.png") in plot_patch.paths + assert path.join(plot_path, "fit_point_fluxes.png") not in plot_patch.paths def test__subplot_fit(