diff --git a/autogalaxy/__init__.py b/autogalaxy/__init__.py index 55e3039af..780a30861 100644 --- a/autogalaxy/__init__.py +++ b/autogalaxy/__init__.py @@ -109,4 +109,4 @@ conf.instance.register(__file__) -__version__ = "2023.3.27.1" +__version__ = "2023.7.7.2" diff --git a/autogalaxy/analysis/analysis.py b/autogalaxy/analysis/analysis.py index 2ef1966fe..42c35dbae 100644 --- a/autogalaxy/analysis/analysis.py +++ b/autogalaxy/analysis/analysis.py @@ -1,9 +1,10 @@ import json import logging import numpy as np -from typing import Optional, Union +from typing import Callable, Dict, Optional, Tuple, Union from os import path import os +import time from autoconf import conf import autofit as af @@ -42,7 +43,9 @@ def __init__(self, cosmology: LensingCosmology = Planck15): """ self.cosmology = cosmology - def plane_via_instance_from(self, instance: af.ModelInstance) -> Plane: + def plane_via_instance_from( + self, instance: af.ModelInstance, run_time_dict: Optional[Dict] = None + ) -> Plane: """ Create a `Plane` from the galaxies contained in a model instance. @@ -57,8 +60,134 @@ def plane_via_instance_from(self, instance: af.ModelInstance) -> Plane: An instance of the Plane class that is used to then fit the dataset. """ if hasattr(instance, "clumps"): - return Plane(galaxies=instance.galaxies + instance.clumps) - return Plane(galaxies=instance.galaxies) + return Plane( + galaxies=instance.galaxies + instance.clumps, + run_time_dict=run_time_dict, + ) + return Plane(galaxies=instance.galaxies, run_time_dict=run_time_dict) + + @property + def fit_func(self) -> Callable: + raise NotImplementedError + + def profile_log_likelihood_function( + self, instance: af.ModelInstance, paths: Optional[af.DirectoryPaths] = None + ) -> Tuple[Dict, Dict]: + """ + This function is optionally called throughout a model-fit to profile the log likelihood function. + + All function calls inside the `log_likelihood_function` that are decorated with the `profile_func` are timed + with their times stored in a dictionary called the `run_time_dict`. + + An `info_dict` is also created which stores information on aspects of the model and dataset that dictate + run times, so the profiled times can be interpreted with this context. + + The results of this profiling are then output to hard-disk in the `preloads` folder of the model-fit results, + which they can be inspected to ensure run-times are as expected. + + Parameters + ---------- + 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). + paths + The PyAutoFit 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. + + Returns + ------- + Two dictionaries, the profiling dictionary and info dictionary, which contain the profiling times of the + `log_likelihood_function` and information on the model and dataset used to perform the profiling. + """ + run_time_dict = {} + info_dict = {} + + repeats = conf.instance["general"]["profiling"]["repeats"] + info_dict["repeats"] = repeats + + fit = self.fit_func(instance=instance) + fit.figure_of_merit + + start = time.time() + + for i in range(repeats): + try: + fit = self.fit_func(instance=instance) + fit.figure_of_merit + except Exception: + logger.info( + "Profiling failed. Returning without outputting information." + ) + return + + fit_time = (time.time() - start) / repeats + + run_time_dict["fit_time"] = fit_time + + fit = self.fit_func(instance=instance, run_time_dict=run_time_dict) + fit.figure_of_merit + + try: + info_dict["image_pixels"] = self.dataset.grid.sub_shape_slim + info_dict["sub_size_light_profiles"] = self.dataset.grid.sub_size + except AttributeError: + pass + + if fit.model_obj.has(cls=aa.Pixelization): + info_dict["use_w_tilde"] = fit.inversion.settings.use_w_tilde + info_dict["sub_size_pixelization"] = self.dataset.grid_pixelization.sub_size + info_dict[ + "use_positive_only_solver" + ] = fit.inversion.settings.use_positive_only_solver + info_dict[ + "force_edge_pixels_to_zeros" + ] = fit.inversion.settings.force_edge_pixels_to_zeros + info_dict["use_w_tilde_numpy"] = fit.inversion.settings.use_w_tilde_numpy + info_dict["source_pixels"] = len(fit.inversion.reconstruction) + + if hasattr(fit.inversion, "w_tilde"): + info_dict[ + "w_tilde_curvature_preload_size" + ] = fit.inversion.w_tilde.curvature_preload.shape[0] + + self.output_profiling_info( + paths=paths, run_time_dict=run_time_dict, info_dict=info_dict + ) + + return run_time_dict, info_dict + + def output_profiling_info( + self, paths: Optional[af.DirectoryPaths], run_time_dict: Dict, info_dict: Dict + ): + """ + Output the log likelihood function profiling information to hard-disk as a json file. + + This function is separate from the `profile_log_likelihood_function` function above such that it can be + called by children `Analysis` classes that profile additional aspects of the model-fit and therefore add + extra information to the `run_time_dict` and `info_dict`. + + Parameters + ---------- + paths + The PyAutoFit 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. + run_time_dict + A dictionary containing the profiling times of the functions called by the `log_likelihood_function`. + info_dict + A dictionary containing information on the model and dataset used to perform the profiling, where these + settings typically control the overall run-time. + """ + + if paths is None: + return + + os.makedirs(paths.profile_path, exist_ok=True) + + with open(path.join(paths.profile_path, "run_time_dict.json"), "w+") as f: + json.dump(run_time_dict, f, indent=4) + + with open(path.join(paths.profile_path, "info_dict.json"), "w+") as f: + json.dump(info_dict, f, indent=4) class AnalysisDataset(Analysis): @@ -419,7 +548,3 @@ def output_or_check_figure_of_merit_sanity( f"Old Figure of Merit = {figure_of_merit_sanity}\n" f"New Figure of Merit = {figure_of_merit}" ) - - @property - def fit_func(self): - raise NotImplementedError diff --git a/autogalaxy/config/general.yaml b/autogalaxy/config/general.yaml index 2060a19cc..ee88a811c 100644 --- a/autogalaxy/config/general.yaml +++ b/autogalaxy/config/general.yaml @@ -11,4 +11,4 @@ test: bypass_figure_of_merit_sanity: false check_preloads: false exception_override: false - preloads_check_threshold: 0.1 # If the figure of merit of a fit with and without preloads is greater than this threshold, the check preload test fails and an exception raised for a model-fit. + preloads_check_threshold: 1.0 # If the figure of merit of a fit with and without preloads is greater than this threshold, the check preload test fails and an exception raised for a model-fit. diff --git a/autogalaxy/imaging/fit_imaging.py b/autogalaxy/imaging/fit_imaging.py index ba77abefe..c7fa27755 100644 --- a/autogalaxy/imaging/fit_imaging.py +++ b/autogalaxy/imaging/fit_imaging.py @@ -25,7 +25,7 @@ def __init__( settings_pixelization: aa.SettingsPixelization = aa.SettingsPixelization(), settings_inversion: aa.SettingsInversion = aa.SettingsInversion(), preloads: aa.Preloads = Preloads(), - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ): """ Fits an imaging dataset using a `Plane` object. @@ -65,7 +65,7 @@ def __init__( preloads Contains preloaded calculations (e.g. linear algebra matrices) which can skip certain calculations in the fit. - profiling_dict + run_time_dict A dictionary which if passed to the fit records how long fucntion calls which have the `profile_func` decorator take to run. """ @@ -75,7 +75,7 @@ def __init__( super().__init__( dataset=dataset, - profiling_dict=profiling_dict, + run_time_dict=run_time_dict, ) AbstractFitInversion.__init__( self=self, model_obj=plane, settings_inversion=settings_inversion @@ -145,6 +145,7 @@ def plane_to_inversion(self) -> PlaneToInversion: settings_pixelization=self.settings_pixelization, settings_inversion=self.settings_inversion, preloads=self.preloads, + run_time_dict=self.run_time_dict, ) @cached_property @@ -304,7 +305,7 @@ def refit_with_new_preloads( ------- A new fit which has used new preloads input into this function but the same dataset, plane and other settings. """ - profiling_dict = {} if self.profiling_dict is not None else None + run_time_dict = {} if self.run_time_dict is not None else None settings_inversion = ( self.settings_inversion @@ -318,5 +319,5 @@ def refit_with_new_preloads( settings_pixelization=self.settings_pixelization, settings_inversion=settings_inversion, preloads=preloads, - profiling_dict=profiling_dict, + run_time_dict=run_time_dict, ) diff --git a/autogalaxy/imaging/model/analysis.py b/autogalaxy/imaging/model/analysis.py index a61d40d80..14f42267d 100644 --- a/autogalaxy/imaging/model/analysis.py +++ b/autogalaxy/imaging/model/analysis.py @@ -1,6 +1,6 @@ import numpy as np -import os -from typing import Dict, Optional + +from typing import Dict, Optional, Tuple import autofit as af import autoarray as aa @@ -151,7 +151,7 @@ def fit_imaging_via_instance_from( self, instance: af.ModelInstance, preload_overwrite: Optional[Preloads] = None, - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ) -> FitImaging: """ Given a model instance create a `FitImaging` object. @@ -166,7 +166,7 @@ def fit_imaging_via_instance_from( via a non-linear search). preload_overwrite If a `Preload` object is input this is used instead of the preloads stored as an attribute in the analysis. - profiling_dict + run_time_dict A dictionary which times functions called to fit the model to data, for profiling. Returns @@ -176,19 +176,21 @@ def fit_imaging_via_instance_from( """ instance = self.instance_with_associated_adapt_images_from(instance=instance) - plane = self.plane_via_instance_from(instance=instance) + plane = self.plane_via_instance_from( + instance=instance, run_time_dict=run_time_dict + ) return self.fit_imaging_via_plane_from( plane=plane, preload_overwrite=preload_overwrite, - profiling_dict=profiling_dict, + run_time_dict=run_time_dict, ) def fit_imaging_via_plane_from( self, plane: Plane, preload_overwrite: Optional[Preloads] = None, - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ) -> FitImaging: """ Given a `Plane`, which the analysis constructs from a model instance, create a `FitImaging` object. @@ -202,7 +204,7 @@ def fit_imaging_via_plane_from( The plane of galaxies whose model images are used to fit the imaging data. preload_overwrite If a `Preload` object is input this is used instead of the preloads stored as an attribute in the analysis. - profiling_dict + run_time_dict A dictionary which times functions called to fit the model to data, for profiling. Returns @@ -219,7 +221,7 @@ def fit_imaging_via_plane_from( settings_pixelization=self.settings_pixelization, settings_inversion=self.settings_inversion, preloads=preloads, - profiling_dict=profiling_dict, + run_time_dict=run_time_dict, ) @property @@ -400,3 +402,44 @@ def save_attributes_for_aggregator(self, paths: af.DirectoryPaths): paths.save_object("psf", self.dataset.psf) paths.save_object("mask", self.dataset.mask) + + def profile_log_likelihood_function( + self, instance: af.ModelInstance, paths: Optional[af.DirectoryPaths] = None + ) -> Tuple[Dict, Dict]: + """ + This function is optionally called throughout a model-fit to profile the log likelihood function. + + All function calls inside the `log_likelihood_function` that are decorated with the `profile_func` are timed + with their times stored in a dictionary called the `run_time_dict`. + + An `info_dict` is also created which stores information on aspects of the model and dataset that dictate + run times, so the profiled times can be interpreted with this context. + + The results of this profiling are then output to hard-disk in the `preloads` folder of the model-fit results, + which they can be inspected to ensure run-times are as expected. + + Parameters + ---------- + 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). + paths + The PyAutoFit 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. + + Returns + ------- + Two dictionaries, the profiling dictionary and info dictionary, which contain the profiling times of the + `log_likelihood_function` and information on the model and dataset used to perform the profiling. + """ + run_time_dict, info_dict = super().profile_log_likelihood_function( + instance=instance, + ) + + info_dict["psf_shape_2d"] = self.dataset.psf.shape_native + + self.output_profiling_info( + paths=paths, run_time_dict=run_time_dict, info_dict=info_dict + ) + + return run_time_dict, info_dict diff --git a/autogalaxy/interferometer/fit_interferometer.py b/autogalaxy/interferometer/fit_interferometer.py index dfbb2747a..c1f42f688 100644 --- a/autogalaxy/interferometer/fit_interferometer.py +++ b/autogalaxy/interferometer/fit_interferometer.py @@ -20,7 +20,7 @@ def __init__( settings_pixelization: aa.SettingsPixelization = aa.SettingsPixelization(), settings_inversion: aa.SettingsInversion = aa.SettingsInversion(), preloads: aa.Preloads = Preloads(), - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ): """ Fits an interferometer dataset using a `Plane` object. @@ -61,7 +61,7 @@ def __init__( preloads Contains preloaded calculations (e.g. linear algebra matrices) which can skip certain calculations in the fit. - profiling_dict + run_time_dict A dictionary which if passed to the fit records how long fucntion calls which have the `profile_func` decorator take to run. """ @@ -72,7 +72,7 @@ def __init__( settings_inversion.use_w_tilde = False super().__init__( - dataset=dataset, use_mask_in_fit=False, profiling_dict=profiling_dict + dataset=dataset, use_mask_in_fit=False, run_time_dict=run_time_dict ) AbstractFitInversion.__init__( self=self, model_obj=plane, settings_inversion=settings_inversion @@ -241,10 +241,10 @@ def refit_with_new_preloads( ------- A new fit which has used new preloads input into this function but the same dataset, plane and other settings. """ - if self.profiling_dict is not None: - profiling_dict = {} + if self.run_time_dict is not None: + run_time_dict = {} else: - profiling_dict = None + run_time_dict = None if settings_inversion is None: settings_inversion = self.settings_inversion @@ -255,5 +255,5 @@ def refit_with_new_preloads( settings_pixelization=self.settings_pixelization, settings_inversion=settings_inversion, preloads=preloads, - profiling_dict=profiling_dict, + run_time_dict=run_time_dict, ) diff --git a/autogalaxy/interferometer/model/analysis.py b/autogalaxy/interferometer/model/analysis.py index d2f07a1cb..30868ce57 100644 --- a/autogalaxy/interferometer/model/analysis.py +++ b/autogalaxy/interferometer/model/analysis.py @@ -1,7 +1,6 @@ import logging import numpy as np -import os -from typing import Optional +from typing import Dict, Optional, Tuple import autofit as af import autoarray as aa @@ -15,7 +14,6 @@ from autogalaxy.interferometer.model.result import ResultInterferometer from autogalaxy.interferometer.model.visualizer import VisualizerInterferometer from autogalaxy.interferometer.fit_interferometer import FitInterferometer -from autogalaxy.galaxy.galaxy import Galaxy from autogalaxy.plane.plane import Plane from autogalaxy import exc @@ -161,7 +159,7 @@ def log_likelihood_function(self, instance: af.ModelInstance) -> float: def fit_interferometer_via_instance_from( self, instance: af.ModelInstance, - preload_overwrite: Optional[Preloads] = None, + run_time_dict: Optional[Dict] = None, ) -> FitInterferometer: """ Given a model instance create a `FitInterferometer` object. @@ -176,7 +174,7 @@ def fit_interferometer_via_instance_from( via a non-linear search). preload_overwrite If a `Preload` object is input this is used instead of the preloads stored as an attribute in the analysis. - profiling_dict + run_time_dict A dictionary which times functions called to fit the model to data, for profiling. Returns @@ -186,16 +184,19 @@ def fit_interferometer_via_instance_from( """ instance = self.instance_with_associated_adapt_images_from(instance=instance) - plane = self.plane_via_instance_from(instance=instance) + plane = self.plane_via_instance_from( + instance=instance, run_time_dict=run_time_dict + ) return self.fit_interferometer_via_plane_from( - plane=plane, + plane=plane, run_time_dict=run_time_dict ) def fit_interferometer_via_plane_from( self, plane: Plane, preload_overwrite: Optional[Preloads] = None, + run_time_dict: Optional[Dict] = None, ) -> FitInterferometer: """ Given a `Plane`, which the analysis constructs from a model instance, create a `FitInterferometer` object. @@ -222,6 +223,7 @@ def fit_interferometer_via_plane_from( settings_pixelization=self.settings_pixelization, settings_inversion=self.settings_inversion, preloads=preloads, + run_time_dict=run_time_dict, ) @property @@ -407,3 +409,45 @@ def save_attributes_for_aggregator(self, paths: af.DirectoryPaths): paths.save_object("uv_wavelengths", self.dataset.uv_wavelengths) paths.save_object("real_space_mask", self.dataset.real_space_mask) + + def profile_log_likelihood_function( + self, instance: af.ModelInstance, paths: Optional[af.DirectoryPaths] = None + ) -> Tuple[Dict, Dict]: + """ + This function is optionally called throughout a model-fit to profile the log likelihood function. + + All function calls inside the `log_likelihood_function` that are decorated with the `profile_func` are timed + with their times stored in a dictionary called the `run_time_dict`. + + An `info_dict` is also created which stores information on aspects of the model and dataset that dictate + run times, so the profiled times can be interpreted with this context. + + The results of this profiling are then output to hard-disk in the `preloads` folder of the model-fit results, + which they can be inspected to ensure run-times are as expected. + + Parameters + ---------- + 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). + paths + The PyAutoFit 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. + + Returns + ------- + Two dictionaries, the profiling dictionary and info dictionary, which contain the profiling times of the + `log_likelihood_function` and information on the model and dataset used to perform the profiling. + """ + run_time_dict, info_dict = super().profile_log_likelihood_function( + instance=instance, + ) + + info_dict["number_of_visibilities"] = self.dataset.visibilities.shape[0] + info_dict["transformer_cls"] = self.dataset.transformer.__class__.__name__ + + self.output_profiling_info( + paths=paths, run_time_dict=run_time_dict, info_dict=info_dict + ) + + return run_time_dict, info_dict diff --git a/autogalaxy/interferometer/plot/fit_interferometer_plotters.py b/autogalaxy/interferometer/plot/fit_interferometer_plotters.py index d3d6811f3..f9d4562bd 100644 --- a/autogalaxy/interferometer/plot/fit_interferometer_plotters.py +++ b/autogalaxy/interferometer/plot/fit_interferometer_plotters.py @@ -1,3 +1,4 @@ +import autoarray as aa import autoarray.plot as aplt from autoarray.fit.plot.fit_interferometer_plotters import FitInterferometerPlotterMeta @@ -184,14 +185,14 @@ def subplot_fit_real_space(self): Depending on whether `LightProfile`'s or an `Inversion` are used to represent galaxies in the `Plane`, different methods are called to create these real-space images. """ - if self.fit.inversion is None: + if not self.plane.has(cls=aa.Pixelization): plane_plotter = self.plane_plotter_from(plane=self.plane) plane_plotter.subplot( image=True, plane_image=True, auto_filename="subplot_fit_real_space" ) - elif self.fit.inversion is not None: + elif self.plane.has(cls=aa.Pixelization): self.open_subplot_figure(number_subplots=6) mapper_index = 0 diff --git a/autogalaxy/operate/image.py b/autogalaxy/operate/image.py index ee01df826..1d89bd125 100644 --- a/autogalaxy/operate/image.py +++ b/autogalaxy/operate/image.py @@ -29,6 +29,7 @@ def image_2d_from( def has(self, cls) -> bool: raise NotImplementedError + @aa.profile_func def _blurred_image_2d_from( self, image_2d: aa.Array2D, @@ -168,6 +169,7 @@ def unmasked_blurred_image_2d_from(self, grid, psf): return padded_image_2d + padded_image_2d_operated.binned + @aa.profile_func def visibilities_from( self, grid: aa.Grid2D, transformer: aa.type.Transformer ) -> aa.Visibilities: diff --git a/autogalaxy/plane/plane.py b/autogalaxy/plane/plane.py index 594b4925b..5e618913d 100644 --- a/autogalaxy/plane/plane.py +++ b/autogalaxy/plane/plane.py @@ -1,6 +1,6 @@ import json import numpy as np -from typing import Dict, List, Optional, Tuple, Type +from typing import Dict, List, Optional, Tuple, Type, Union import autoarray as aa @@ -23,7 +23,7 @@ def __init__( self, galaxies, redshift: Optional[float] = None, - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ): """ A plane of galaxies where all galaxies are at the same redshift. @@ -52,7 +52,7 @@ def __init__( self.redshift = redshift self.galaxies = galaxies - self.profiling_dict = profiling_dict + self.run_time_dict = run_time_dict def dict(self) -> Dict: plane_dict = super().dict() @@ -67,7 +67,7 @@ def output_to_json(self, file_path: str): def galaxy_redshifts(self) -> List[float]: return [galaxy.redshift for galaxy in self.galaxies] - def has(self, cls: Tuple[Type]) -> bool: + def has(self, cls: Union[Type, Tuple[Type]]) -> bool: if self.galaxies is not None: return any(list(map(lambda galaxy: galaxy.has(cls=cls), self.galaxies))) return False diff --git a/autogalaxy/plane/plane_util.py b/autogalaxy/plane/plane_util.py index 9c5403176..f7482302e 100644 --- a/autogalaxy/plane/plane_util.py +++ b/autogalaxy/plane/plane_util.py @@ -209,7 +209,7 @@ def galaxies_in_redshift_ordered_planes_from(galaxies, plane_redshifts): return galaxies_in_redshift_ordered_planes -def planes_via_galaxies_from(galaxies, profiling_dict=None, plane_cls=Plane): +def planes_via_galaxies_from(galaxies, run_time_dict=None, plane_cls=Plane): plane_redshifts = ordered_plane_redshifts_from(galaxies=galaxies) galaxies_in_planes = galaxies_in_redshift_ordered_planes_from( @@ -221,7 +221,7 @@ def planes_via_galaxies_from(galaxies, profiling_dict=None, plane_cls=Plane): for plane_index in range(0, len(plane_redshifts)): planes.append( plane_cls( - galaxies=galaxies_in_planes[plane_index], profiling_dict=profiling_dict + galaxies=galaxies_in_planes[plane_index], run_time_dict=run_time_dict ) ) diff --git a/autogalaxy/plane/plot/plane_plotters.py b/autogalaxy/plane/plot/plane_plotters.py index 37c3d62df..ee4b6ad41 100644 --- a/autogalaxy/plane/plot/plane_plotters.py +++ b/autogalaxy/plane/plot/plane_plotters.py @@ -133,6 +133,7 @@ def figures_2d( zoom_to_brightest: bool = True, title_suffix: str = "", filename_suffix: str = "", + source_plane_title: bool = False, ): """ Plots the individual attributes of the plotter's `Plane` object in 2D, which are computed via the plotter's 2D @@ -166,6 +167,8 @@ def figures_2d( Add a suffix to the end of the matplotlib title label. filename_suffix Add a suffix to the end of the filename the plot is saved to hard-disk using. + source_plane_title + If `True`, the title of the plot is overwritten to read "source-plane image". """ if image: self.mat_plot_2d.plot_array( @@ -177,18 +180,10 @@ def figures_2d( ) if plane_image: - import numpy as np - - # print(self.plane.galaxies[0].bulge) - # print(self.plane.galaxies[0].bulge.intensity) - # - # print(self.plane.plane_image_2d_from( - # grid=self.grid, zoom_to_brightest=zoom_to_brightest - # )) - # print(np.max(self.plane.plane_image_2d_from( - # grid=self.grid, zoom_to_brightest=zoom_to_brightest - # ))) - # www + if source_plane_title: + title = "Source Plane Image" + else: + title = f"Plane Image{title_suffix}" self.mat_plot_2d.plot_array( array=self.plane.plane_image_2d_from( @@ -196,17 +191,22 @@ def figures_2d( ), visuals_2d=self.get_visuals_2d(), auto_labels=aplt.AutoLabels( - title=f"Plane Image{title_suffix}", + title=title, filename=f"plane_image{filename_suffix}", ), ) if plane_grid: + if source_plane_title: + title = "Source Plane Grid" + else: + title = f"Plane Grid{title_suffix}" + self.mat_plot_2d.plot_grid( grid=self.grid, visuals_2d=self.get_visuals_2d(), auto_labels=aplt.AutoLabels( - title=f"Plane Grid2D{title_suffix}", + title=title, filename=f"plane_grid{filename_suffix}", ), ) diff --git a/autogalaxy/plane/to_inversion.py b/autogalaxy/plane/to_inversion.py index ada28336b..9f350cd5d 100644 --- a/autogalaxy/plane/to_inversion.py +++ b/autogalaxy/plane/to_inversion.py @@ -29,7 +29,7 @@ def __init__( settings_pixelization=aa.SettingsPixelization(), settings_inversion: aa.SettingsInversion = aa.SettingsInversion(), preloads=Preloads(), - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ): if dataset is not None: if dataset.noise_covariance_matrix is not None: @@ -52,7 +52,7 @@ def __init__( self.settings_inversion = settings_inversion self.preloads = preloads - self.profiling_dict = profiling_dict + self.run_time_dict = run_time_dict def cls_light_profile_func_list_galaxy_dict_from( self, cls: Type @@ -102,7 +102,7 @@ def __init__( settings_pixelization=aa.SettingsPixelization(), settings_inversion: aa.SettingsInversion = aa.SettingsInversion(), preloads=aa.Preloads(), - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ): self.plane = plane @@ -114,7 +114,7 @@ def __init__( settings_pixelization=settings_pixelization, settings_inversion=settings_inversion, preloads=preloads, - profiling_dict=profiling_dict, + run_time_dict=run_time_dict, ) if grid is not None: @@ -223,7 +223,7 @@ def mapper_from( adapt_data=adapt_galaxy_image, settings=self.settings_pixelization, preloads=self.preloads, - profiling_dict=self.plane.profiling_dict, + run_time_dict=self.plane.run_time_dict, ) return mapper_from(mapper_grids=mapper_grids, regularization=regularization) @@ -268,7 +268,7 @@ def inversion(self) -> aa.AbstractInversion: linear_obj_list=self.linear_obj_list, settings=self.settings_inversion, preloads=self.preloads, - profiling_dict=self.plane.profiling_dict, + run_time_dict=self.plane.run_time_dict, ) inversion.linear_obj_galaxy_dict = self.linear_obj_galaxy_dict diff --git a/autogalaxy/plot/mass_plotter.py b/autogalaxy/plot/mass_plotter.py index e6c841810..2e9359ae3 100644 --- a/autogalaxy/plot/mass_plotter.py +++ b/autogalaxy/plot/mass_plotter.py @@ -108,7 +108,7 @@ def figures_2d( array=deflections_x, visuals_2d=self.get_visuals_2d(), auto_labels=aplt.AutoLabels( - title=f"deflections X{title_suffix}", + title=f"Deflections X{title_suffix}", filename=f"deflections_x_2d{filename_suffix}", ), ) diff --git a/autogalaxy/profiles/light/linear/abstract.py b/autogalaxy/profiles/light/linear/abstract.py index 34f8a95ca..84d8cd962 100644 --- a/autogalaxy/profiles/light/linear/abstract.py +++ b/autogalaxy/profiles/light/linear/abstract.py @@ -107,7 +107,7 @@ def __init__( convolver: Optional[aa.Convolver], light_profile_list: List[LightProfileLinear], regularization=aa.reg.Regularization, - profiling_dict: Optional[Dict] = None, + run_time_dict: Optional[Dict] = None, ): for light_profile in light_profile_list: if not isinstance(light_profile, LightProfileLinear): @@ -121,7 +121,7 @@ def __init__( ) super().__init__( - grid=grid, regularization=regularization, profiling_dict=profiling_dict + grid=grid, regularization=regularization, run_time_dict=run_time_dict ) self.blurring_grid = blurring_grid diff --git a/docs/index.rst b/docs/index.rst index 06291eeaf..7b0c1f6d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ irregulars. Today, by analysing millions of galaxies with advanced image process expanded on this picture and revealed the rich diversity of galaxy morphology both in the nearby and distant Universe. -``PyAutoGalaxy`` is an open-source Python 3.8+ package for analysing the morphologies and structures of large +``PyAutoGalaxy`` is an open-source Python 3.8 - 3.10 package for analysing the morphologies and structures of large multi-wavelength galaxy samples. **PyAutoGalaxy** makes it simple to model galaxies, for example this Hubble Space Telescope imaging of a spiral galaxy: diff --git a/docs/installation/conda.rst b/docs/installation/conda.rst index e567f28b6..b119e7599 100644 --- a/docs/installation/conda.rst +++ b/docs/installation/conda.rst @@ -47,7 +47,7 @@ You may get warnings which state something like: .. code-block:: bash - ERROR: autoarray 2022.2.14.1 has requirement numpy<=1.22.1, but you'll have numpy 1.22.2 which is incompatible. + ERROR: autoarray 2023.7.7.2 has requirement numpy<=1.22.1, but you'll have numpy 1.22.2 which is incompatible. ERROR: numba 0.53.1 has requirement llvmlite<0.37,>=0.36.0rc1, but you'll have llvmlite 0.38.0 which is incompatible. If you see these messages, they do not mean that the installation has failed and the instructions below will diff --git a/docs/installation/overview.rst b/docs/installation/overview.rst index eaa2848b3..6630edaf8 100644 --- a/docs/installation/overview.rst +++ b/docs/installation/overview.rst @@ -3,7 +3,7 @@ Overview ======== -**PyAutoGalaxy** requires Python 3.8+ and support the Linux, MacOS and Windows operating systems. +**PyAutoGalaxy** requires Python 3.8 - 3.10 and support the Linux, MacOS and Windows operating systems. **PyAutoGalaxy** can be installed via the Python distribution `Anaconda `_ or using `PyPI `_ to ``pip install`` **PyAutoGalaxy** into your Python distribution. diff --git a/docs/installation/pip.rst b/docs/installation/pip.rst index e252bc83e..ca89c6ec5 100644 --- a/docs/installation/pip.rst +++ b/docs/installation/pip.rst @@ -27,7 +27,7 @@ You may get warnings which state something like: .. code-block:: bash - ERROR: autoarray 2022.2.14.1 has requirement numpy<=1.22.1, but you'll have numpy 1.22.2 which is incompatible. + ERROR: autoarray 2023.7.7.2 has requirement numpy<=1.22.1, but you'll have numpy 1.22.2 which is incompatible. ERROR: numba 0.53.1 has requirement llvmlite<0.37,>=0.36.0rc1, but you'll have llvmlite 0.38.0 which is incompatible. If you see these messages, they do not mean that the installation has failed and the instructions below will diff --git a/docs/installation/troubleshooting.rst b/docs/installation/troubleshooting.rst index 400b12edc..b8724e3ae 100644 --- a/docs/installation/troubleshooting.rst +++ b/docs/installation/troubleshooting.rst @@ -14,42 +14,6 @@ the latest version of pip. pip install --upgrade pip pip3 install --upgrade pip -NumPy / numba -------------- - -The libraries ``numpy`` and ``numba`` can be installed with incompatible versions. - -An error message like the one below occurs when importing **PyAutoGalaxy**: - -.. code-block:: bash - - Traceback (most recent call last): - File "", line 1, in - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/autolens/__init__.py", line 1, in - from autoarray import preprocess - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/autoarray/__init__.py", line 2, in - from . import type - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/autoarray/type.py", line 7, in - from autoarray.mask.mask_1d import Mask1D - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/autoarray/mask/mask_1d.py", line 8, in - from autoarray.structures.arrays import array_1d_util - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/autoarray/structures/arrays/array_1d_util.py", line 5, in - from autoarray import numba_util - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/autoarray/numba_util.py", line 2, in - import numba - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/numba/__init__.py", line 200, in - _ensure_critical_deps() - File "/home/jammy/venvs/PyAutoMay2/lib/python3.8/site-packages/numba/__init__.py", line 140, in _ensure_critical_deps - raise ImportError("Numba needs NumPy 1.21 or less") - ImportError: Numba needs NumPy 1.21 or less - -This can be fixed by reinstalling numpy with the version requested by the error message, in the example -numpy 1.21 (you should replace the ``==1.21.0`` with a different version if requested). - -.. code-block:: bash - - pip install numpy==1.21.0 - Pip / Conda ----------- @@ -91,15 +55,15 @@ Matplotlib uses the default backend on your computer, as set in the config file: .. code-block:: bash - autogalaxy_workspace/config/visualize/generaltrue + autogalaxy_workspace/config/visualize/general.yaml If unchanged, the backend is set to 'default', meaning it will use the backend automatically set up for Python on your system. .. code-block:: bash - [general] - backend = default + general: + backend: default There have been reports that using the default backend causes crashes when running the test script below (either the code crashes without a error or your computer restarts). If this happens, change the config's backend until the test @@ -107,5 +71,5 @@ works (TKAgg has worked on Linux machines, Qt5Agg has worked on new MACs). For e .. code-block:: bash - [general] - backend = TKAgg + general: + backend: TKAgg diff --git a/docs/overview/overview_2_fitting.rst b/docs/overview/overview_2_fitting.rst index e759c06fd..881ef6f20 100644 --- a/docs/overview/overview_2_fitting.rst +++ b/docs/overview/overview_2_fitting.rst @@ -26,7 +26,7 @@ demonstrate fitting. ) dataset_plotter = aplt.ImagingPlotter(dataset=dataset) - dataset_plotter.figures_2d(image=True, noise_map=True, psf=True) + dataset_plotter.figures_2d(data=True, noise_map=True, psf=True) Here's what our ``image``, ``noise_map`` and ``psf`` (point-spread function) look like: @@ -58,7 +58,7 @@ To do this we can use a ``Mask2D`` object, which for this example we'll create a dataset = dataset.apply_mask(mask=mask) dataset_plotter = aplt.ImagingPlotter(dataset=dataset) - dataset_plotter.figures_2d(image=True) + dataset_plotter.figures_2d(data=True) Here is what our image looks like with the mask applied, where **PyAutoGalaxy** has automatically zoomed around the ``Mask2D`` to make the lensed source appear bigger: diff --git a/docs/overview/overview_3_modeling.rst b/docs/overview/overview_3_modeling.rst index 5ec1ce4c7..ef29396b1 100644 --- a/docs/overview/overview_3_modeling.rst +++ b/docs/overview/overview_3_modeling.rst @@ -114,12 +114,73 @@ Analysis -------- We next create an ``AnalysisImaging`` object, which contains the ``log likelihood function`` that the non-linear -search calls to fit the lens model to the data. +search calls to fit the model to the data. .. code-block:: python analysis = ag.AnalysisImaging(dataset=dataset) + +Run Times +--------- + +modeling can be a computationally expensive process. When fitting complex models to high resolution datasets +run times can be of order hours, days, weeks or even months. + +Run times are dictated by two factors: + + - The log likelihood evaluation time: the time it takes for a single ``instance`` of the model to be fitted to + the dataset such that a log likelihood is returned. + + - The number of iterations (e.g. log likelihood evaluations) performed by the non-linear search: more complex lens + models require more iterations to converge to a solution. + +The log likelihood evaluation time can be estimated before a fit using the ``profile_log_likelihood_function`` method, +which returns two dictionaries containing the run-times and information about the fit. + +.. code-block:: python + + run_time_dict, info_dict = analysis.profile_log_likelihood_function( + instance=model.random_instance() + ) + +The overall log likelihood evaluation time is given by the ``fit_time`` key. + +For this example, it is ~0.01 seconds, which is extremely fast for modeling. More advanced lens +modeling features (e.g. shapelets, multi Gaussian expansions, pixelizations) have slower log likelihood evaluation +times (1-3 seconds), and you should be wary of this when using these features. + +The ``run_time_dict`` has a break-down of the run-time of every individual function call in the log likelihood +function, whereas the ``info_dict`` stores information about the data which drives the run-time (e.g. number of +image-pixels in the mask, the shape of the PSF, etc.). + +.. code-block:: python + + print(f"Log Likelihood Evaluation Time (second) = {run_time_dict['fit_time']}") + +This gives an output of ~0.01 seconds. + +To estimate the expected overall run time of the model-fit we multiply the log likelihood evaluation time by an +estimate of the number of iterations the non-linear search will perform. + +Estimating this quantity is more tricky, as it varies depending on the model complexity (e.g. number of parameters) +and the properties of the dataset and model being fitted. + +For this example, we conservatively estimate that the non-linear search will perform ~10000 iterations per free +parameter in the model. This is an upper limit, with models typically converging in far fewer iterations. + +If you perform the fit over multiple CPUs, you can divide the run time by the number of cores to get an estimate of +the time it will take to fit the model. However, above ~6 cores the speed-up from parallelization is less efficient and +does not scale linearly with the number of cores. + +.. code-block:: python + + print( + "Estimated Run Time Upper Limit (seconds) = ", + (run_time_dict["fit_time"] * model.total_free_parameters * 10000) + / search.number_of_cores, + ) + Model-Fit --------- @@ -133,7 +194,7 @@ dynesty samples, model parameters, visualization) to hard-disk. The non-linear search fits the model by guessing many models over and over iteratively, using the models which give a good fit to the data to guide it where to guess subsequent model. -An animation of a non-linear search is shown below, although this is for a strong gravitational lens using +An animation of a non-linear search is shown below, although this is for a strong gravitational using **PyAutoGalaxy**'s child project **PyAutoLens**. Updating the animation for a galaxy is on the **PyAutoGalaxy** to-do list! @@ -256,7 +317,7 @@ This gives the following output: This result contains the full posterior information of our non-linear search, including all -parameter samples, log likelihood values and tools to compute the errors on the lens model. +parameter samples, log likelihood values and tools to compute the errors on the model. This is contained in the ``Samples`` object. Below, we show how to print the median PDF parameter estimates, but many different results are available and illustrated in the `results package of the workspace `_. diff --git a/paper/paper.md b/paper/paper.md index 38bd3931f..5439f26b8 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -66,7 +66,7 @@ bibliography: paper.bib Nearly a century ago, Edwin Hubble famously classified galaxies into three distinct groups: ellipticals, spirals and irregulars [@Hubble1926]. Today, by analysing millions of galaxies with advanced image processing techniques Astronomers have expanded on this picture and revealed the rich diversity of galaxy morphology in both the nearby and distant -Universe [@Kormendy2015a; @Vulcani2014; @VanDerWel2012]. `PyAutoGalaxy` is an open-source Python 3.8+ package +Universe [@Kormendy2015a; @Vulcani2014; @VanDerWel2012]. `PyAutoGalaxy` is an open-source Python 3.8 - 3.10 package for analysing the morphologies and structures of large multiwavelength galaxy samples, with core features including fully automated Bayesian model-fitting of galaxy two-dimensional surface brightness profiles, support for dataset and interferometer datasets and comprehensive tools for simulating galaxy images. The software places a focus @@ -161,7 +161,7 @@ taken without a local `PyAutoGalaxy` installation. # Software Citations -`PyAutoGalaxy` is written in Python 3.8+ and uses the following software packages: +`PyAutoGalaxy` is written in Python 3.8 - 3.10 and uses the following software packages: - `Astropy` [@astropy1; @astropy2] - `COLOSSUS` [@colossus] diff --git a/test_autogalaxy/config/general.yaml b/test_autogalaxy/config/general.yaml index 4b7f0e2dc..8e7f12e42 100644 --- a/test_autogalaxy/config/general.yaml +++ b/test_autogalaxy/config/general.yaml @@ -42,4 +42,4 @@ test: bypass_figure_of_merit_sanity: false check_preloads: false exception_override: false - preloads_check_threshold: 0.1 # If the figure of merit of a fit with and without preloads is greater than this threshold, the check preload test fails and an exception raised for a model-fit. + preloads_check_threshold: 1.0 # If the figure of merit of a fit with and without preloads is greater than this threshold, the check preload test fails and an exception raised for a model-fit. diff --git a/test_autogalaxy/imaging/model/test_analysis_imaging.py b/test_autogalaxy/imaging/model/test_analysis_imaging.py index 273f2437e..49f86611e 100644 --- a/test_autogalaxy/imaging/model/test_analysis_imaging.py +++ b/test_autogalaxy/imaging/model/test_analysis_imaging.py @@ -37,3 +37,25 @@ def test__figure_of_merit__matches_correct_fit_given_galaxy_profiles( fit = ag.FitImaging(dataset=masked_imaging_7x7, plane=plane) assert fit.log_likelihood == fit_figure_of_merit + + +def test__profile_log_likelihood_function(masked_imaging_7x7): + pixelization = ag.Pixelization( + mesh=ag.mesh.Rectangular(shape=(3, 3)), + regularization=ag.reg.Constant(coefficient=1.0), + ) + + galaxy = ag.Galaxy(redshift=0.5, pixelization=pixelization) + + model = af.Collection(galaxies=af.Collection(galaxy=galaxy)) + + instance = model.instance_from_unit_vector([]) + + analysis = ag.AnalysisImaging(dataset=masked_imaging_7x7) + + run_time_dict, info_dict = analysis.profile_log_likelihood_function( + instance=instance + ) + + assert "regularization_term_0" in run_time_dict + assert "log_det_regularization_matrix_term_0" in run_time_dict diff --git a/test_autogalaxy/interferometer/model/test_analysis_interferometer.py b/test_autogalaxy/interferometer/model/test_analysis_interferometer.py index 508bc1a74..47f65f9b4 100644 --- a/test_autogalaxy/interferometer/model/test_analysis_interferometer.py +++ b/test_autogalaxy/interferometer/model/test_analysis_interferometer.py @@ -38,3 +38,25 @@ def test__fit_figure_of_merit__matches_correct_fit_given_galaxy_profiles( fit = ag.FitInterferometer(dataset=interferometer_7, plane=plane) assert fit.log_likelihood == fit_figure_of_merit + + +def test__profile_log_likelihood_function(interferometer_7): + pixelization = ag.Pixelization( + mesh=ag.mesh.Rectangular(shape=(3, 3)), + regularization=ag.reg.Constant(coefficient=1.0), + ) + + galaxy = ag.Galaxy(redshift=0.5, pixelization=pixelization) + + model = af.Collection(galaxies=af.Collection(galaxy=galaxy)) + + instance = model.instance_from_unit_vector([]) + + analysis = ag.AnalysisInterferometer(dataset=interferometer_7) + + run_time_dict, info_dict = analysis.profile_log_likelihood_function( + instance=instance + ) + + assert "regularization_term_0" in run_time_dict + assert "log_det_regularization_matrix_term_0" in run_time_dict diff --git a/zenodo.json b/zenodo.json index 03b18040a..60fb4a06e 100644 --- a/zenodo.json +++ b/zenodo.json @@ -2,7 +2,7 @@ "description": "Release which is tied to the publication of PyAutoGalaxy in the Journal of Open Source software (JOSS).", "license": "other-open", "title": "PyAutoGalaxy: Open-Source Multiwavelength Galaxy Structure & Morphology", - "version": "2023.3.27.1", + "version": "2023.7.7.2", "upload_type": "software", "publication_date": "2023-01-19", "creators": [ @@ -63,7 +63,7 @@ "related_identifiers": [ { "scheme": "url", - "identifier": "https://github.com/Jammy2211/PyAutoGalaxy/tree/2023.3.27.1", + "identifier": "https://github.com/Jammy2211/PyAutoGalaxy/tree/2023.7.7.2", "relation": "isSupplementTo" }, {