diff --git a/autocti/aggregator/imaging_ci.py b/autocti/aggregator/imaging_ci.py index 1bb1b8fe..89bd7ea2 100644 --- a/autocti/aggregator/imaging_ci.py +++ b/autocti/aggregator/imaging_ci.py @@ -89,6 +89,7 @@ def values_from(hdu: int) -> aa.Array2D: cosmic_ray_map=cosmic_ray_map, settings_dict=settings_dict, layout=layout, + check_noise_map=False, ) dataset_list.append(dataset.apply_mask(mask=mask)) diff --git a/autocti/charge_injection/imaging/imaging.py b/autocti/charge_injection/imaging/imaging.py index 3c0b78db..dd5abdca 100644 --- a/autocti/charge_injection/imaging/imaging.py +++ b/autocti/charge_injection/imaging/imaging.py @@ -24,8 +24,11 @@ def __init__( noise_scaling_map_dict: Optional[Dict] = None, fpr_value: Optional[float] = None, settings_dict: Optional[Dict] = None, + check_noise_map: bool = True, ): - super().__init__(data=data, noise_map=noise_map) + super().__init__( + data=data, noise_map=noise_map, check_noise_map=check_noise_map + ) self.data = self.data.native self.noise_map = self.noise_map.native @@ -319,7 +322,9 @@ def output_to_fits( exception is raised. """ fitsable.output_to_fits( - values=np.asarray(self.data.native), file_path=data_path, overwrite=overwrite + values=np.asarray(self.data.native), + file_path=data_path, + overwrite=overwrite, ) if noise_map_path is not None: diff --git a/autocti/charge_injection/model/plotter.py b/autocti/charge_injection/model/plotter.py index 41023ee2..71b8bff6 100644 --- a/autocti/charge_injection/model/plotter.py +++ b/autocti/charge_injection/model/plotter.py @@ -7,6 +7,8 @@ from autocti.charge_injection.plot import fit_ci_plots from autocti.model.plotter import Plotter, plot_setting +from autoarray import exc as aa_exc + from autocti import exc logger = logging.getLogger(__name__) @@ -85,7 +87,16 @@ def should_plot(name): output_format=self.fmt, title_prefix=self.title_prefix, ) - except (exc.PlottingException, exc.RegionException, TypeError, ValueError): + except ( + exc.PlottingException, + exc.RegionException, + aa_exc.ArrayException, + TypeError, + ValueError, + ): + # Trimmed datasets (e.g. via `apply_settings`) have layouts whose + # extraction regions no longer match the array shape, making the + # binned-FPR diagnostic ill-defined. logger.info( "VISUALIZATION - Could not visualize the ImagingCI binned data" ) diff --git a/autocti/charge_injection/model/result.py b/autocti/charge_injection/model/result.py index babf5a6a..bf8e3a34 100644 --- a/autocti/charge_injection/model/result.py +++ b/autocti/charge_injection/model/result.py @@ -5,17 +5,17 @@ class ResultImagingCI(ResultDataset): @property def max_log_likelihood_full_fit(self) -> FitImagingCI: - return self.analysis.fit_via_instance_and_dataset_from( + return self.analysis_unwrapped.fit_via_instance_and_dataset_from( instance=self.instance, - dataset=self.analysis.dataset_full, + dataset=self.analysis_unwrapped.dataset_full, hyper_noise_scale=True, ) @property def max_log_likelihood_full_fit_no_hyper_scaling(self): - return self.analysis.fit_via_instance_and_dataset_from( + return self.analysis_unwrapped.fit_via_instance_and_dataset_from( instance=self.instance, - dataset=self.analysis.dataset_full, + dataset=self.analysis_unwrapped.dataset_full, hyper_noise_scale=False, ) diff --git a/autocti/charge_injection/model/visualizer.py b/autocti/charge_injection/model/visualizer.py index bb97f907..f38d265a 100644 --- a/autocti/charge_injection/model/visualizer.py +++ b/autocti/charge_injection/model/visualizer.py @@ -165,12 +165,15 @@ def visualize_combined( paths: af.DirectoryPaths, instance: af.ModelInstance, during_analysis: bool, + quick_update: bool = False, ): if analyses is None: return fit_list = [ - analysis.fit_via_instance_from(instance=instance) for analysis in analyses + # The factor graph passes one instance per analysis factor. + analysis.fit_via_instance_from(instance=instance_single) + for analysis, instance_single in zip(analyses, instance) ] fpr_value_list = [fit.dataset.fpr_value for fit in fit_list] @@ -180,7 +183,9 @@ def visualize_combined( fpr_value_list=fpr_value_list, ) - region_list = analyses[0].region_list_from(model=instance) + # The factor graph passes one instance per analysis factor; the region + # list is derived from the first (the CTI model is shared across factors). + region_list = analyses[0].region_list_from(model=instance[0]) visualizer = PlotterImagingCI(image_path=paths.image_path) visualizer.fit_combined(fit_list=fit_list, during_analysis=during_analysis) @@ -193,9 +198,9 @@ def visualize_combined( if analyses[0].dataset_full is not None: fit_full_list = [ analysis.fit_via_instance_and_dataset_from( - instance=instance, dataset=analysis.dataset_full + instance=instance_single, dataset=analysis.dataset_full ) - for analysis in analyses + for analysis, instance_single in zip(analyses, instance) ] fit_full_list = analyses[0].in_ascending_fpr_order_from( diff --git a/autocti/charge_injection/plot/fit_ci_plots.py b/autocti/charge_injection/plot/fit_ci_plots.py index 82a34a70..09eb62b9 100644 --- a/autocti/charge_injection/plot/fit_ci_plots.py +++ b/autocti/charge_injection/plot/fit_ci_plots.py @@ -226,6 +226,9 @@ def subplot_fit_list( _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) n = len(fit_list) + if n == 0: + raise ValueError("An empty list was passed to a *_list plot function.") + cols = min(n, 3) rows = (n + cols - 1) // cols @@ -279,6 +282,9 @@ def subplot_fit_region_list( output_format = output_format[0] n = len(fit_list) + if n == 0: + raise ValueError("An empty list was passed to a *_list plot function.") + cols = min(n, 3) rows = (n + cols - 1) // cols diff --git a/autocti/charge_injection/plot/imaging_ci_plots.py b/autocti/charge_injection/plot/imaging_ci_plots.py index a6be1566..2cf2539d 100644 --- a/autocti/charge_injection/plot/imaging_ci_plots.py +++ b/autocti/charge_injection/plot/imaging_ci_plots.py @@ -328,6 +328,9 @@ def subplot_data_region_list( output_format = output_format[0] n = len(dataset_list) + if n == 0: + raise ValueError("An empty list was passed to a *_list plot function.") + cols = min(n, 3) rows = (n + cols - 1) // cols diff --git a/autocti/dataset_1d/dataset_1d/dataset_1d.py b/autocti/dataset_1d/dataset_1d/dataset_1d.py index 5729e3d6..1080fcac 100644 --- a/autocti/dataset_1d/dataset_1d/dataset_1d.py +++ b/autocti/dataset_1d/dataset_1d/dataset_1d.py @@ -182,7 +182,9 @@ def output_to_fits( exception is raised. """ fitsable.output_to_fits( - values=np.asarray(self.data.native), file_path=data_path, overwrite=overwrite + values=np.asarray(self.data.native), + file_path=data_path, + overwrite=overwrite, ) fitsable.output_to_fits( values=np.asarray(self.noise_map.native), diff --git a/autocti/dataset_1d/model/visualizer.py b/autocti/dataset_1d/model/visualizer.py index dfce1f87..b1fbad22 100644 --- a/autocti/dataset_1d/model/visualizer.py +++ b/autocti/dataset_1d/model/visualizer.py @@ -139,12 +139,15 @@ def visualize_combined( paths: af.DirectoryPaths, instance: af.ModelInstance, during_analysis: bool, + quick_update: bool = False, ): if analyses is None: return fit_list = [ - analysis.fit_via_instance_from(instance=instance) for analysis in analyses + # The factor graph passes one instance per analysis factor. + analysis.fit_via_instance_from(instance=instance_single) + for analysis, instance_single in zip(analyses, instance) ] fpr_value_list = [fit.dataset.fpr_value for fit in fit_list] @@ -167,9 +170,9 @@ def visualize_combined( if analyses[0].dataset_full is not None: fit_full_list = [ analysis.fit_via_instance_and_dataset_from( - instance=instance, dataset=analysis.dataset_full + instance=instance_single, dataset=analysis.dataset_full ) - for analysis in analyses + for analysis, instance_single in zip(analyses, instance) ] fit_full_list = analyses[0].in_ascending_fpr_order_from( diff --git a/autocti/dataset_1d/plot/dataset_1d_plots.py b/autocti/dataset_1d/plot/dataset_1d_plots.py index c2e58113..65ed7552 100644 --- a/autocti/dataset_1d/plot/dataset_1d_plots.py +++ b/autocti/dataset_1d/plot/dataset_1d_plots.py @@ -141,6 +141,9 @@ def subplot_dataset_list( suffix = f"_{region}" if region is not None else "" n = len(dataset_list) + if n == 0: + raise ValueError("An empty list was passed to a *_list plot function.") + cols = min(n, 3) rows = (n + cols - 1) // cols diff --git a/autocti/dataset_1d/plot/fit_plots.py b/autocti/dataset_1d/plot/fit_plots.py index 68a30bc5..4e81e811 100644 --- a/autocti/dataset_1d/plot/fit_plots.py +++ b/autocti/dataset_1d/plot/fit_plots.py @@ -163,6 +163,9 @@ def subplot_fit_list( suffix = f"_{region}" if region is not None else "" n = len(fit_list) + if n == 0: + raise ValueError("An empty list was passed to a *_list plot function.") + cols = min(n, 3) rows = (n + cols - 1) // cols diff --git a/autocti/extract/two_d/abstract.py b/autocti/extract/two_d/abstract.py index 815b9647..5ee5e915 100644 --- a/autocti/extract/two_d/abstract.py +++ b/autocti/extract/two_d/abstract.py @@ -498,10 +498,10 @@ def add_gaussian_noise_to( array = array.native for arr, region in zip(array_2d_list, region_list): - array[ - region.y0 : region.y1, region.x0 : region.x1 - ] = aa.preprocess.data_with_gaussian_noise_added( - data=arr, sigma=noise_sigma, seed=noise_seed + array[region.y0 : region.y1, region.x0 : region.x1] = ( + aa.preprocess.data_with_gaussian_noise_added( + data=arr, sigma=noise_sigma, seed=noise_seed + ) ) return array diff --git a/autocti/instruments/acs/image.py b/autocti/instruments/acs/image.py index 9eec7a91..71d98b0e 100644 --- a/autocti/instruments/acs/image.py +++ b/autocti/instruments/acs/image.py @@ -102,12 +102,8 @@ def from_fits( file_path=bias_file_path, hdu=hdu, do_not_scale_image_data=True ) - header_sci_obj = fitsable.header_obj_from( - file_path=bias_file_path, hdu=0 - ) - header_hdu_obj = fitsable.header_obj_from( - file_path=bias_file_path, hdu=hdu - ) + header_sci_obj = fitsable.header_obj_from(file_path=bias_file_path, hdu=0) + header_hdu_obj = fitsable.header_obj_from(file_path=bias_file_path, hdu=hdu) bias_header = HeaderACS( header_sci_obj=header_sci_obj, diff --git a/autocti/mask/mask_2d.py b/autocti/mask/mask_2d.py index eb2804d0..a23b6dea 100644 --- a/autocti/mask/mask_2d.py +++ b/autocti/mask/mask_2d.py @@ -227,9 +227,7 @@ def from_fits( pixel_scales = (float(pixel_scales), float(pixel_scales)) mask = cls.manual( - mask=fitsable.ndarray_via_fits_from( - file_path=file_path, hdu=hdu - ), + mask=fitsable.ndarray_via_fits_from(file_path=file_path, hdu=hdu), pixel_scales=pixel_scales, origin=origin, ) diff --git a/autocti/model/result.py b/autocti/model/result.py index b5d87f79..257ba974 100644 --- a/autocti/model/result.py +++ b/autocti/model/result.py @@ -1,36 +1,49 @@ -from autofit.non_linear import result - - -class Result(result.Result): - def __init__( - self, - samples_summary, - paths=None, - samples=None, - analysis=None, - search_internal=None, - ): - """ - The result of a phase - """ - super().__init__( - samples_summary=samples_summary, - paths=paths, - samples=samples, - search_internal=search_internal, - analysis=analysis, - ) - - @property - def clocker(self): - return self.analysis.clocker - - -class ResultDataset(Result): - @property - def max_log_likelihood_fit(self): - return self.analysis.fit_via_instance_from(instance=self.instance) - - @property - def mask(self): - return self.max_log_likelihood_fit.mask +from autofit.non_linear import result + + +class Result(result.Result): + def __init__( + self, + samples_summary, + paths=None, + samples=None, + analysis=None, + search_internal=None, + ): + """ + The result of a phase + """ + super().__init__( + samples_summary=samples_summary, + paths=paths, + samples=samples, + search_internal=search_internal, + analysis=analysis, + ) + + @property + def analysis_unwrapped(self): + """ + The CTI analysis this result was inferred from. + + A multi-dataset fit via a factor graph gives each child result an + `AnalysisFactor` wrapper as its analysis, which does not delegate + attribute access to the analysis it wraps — so it is unwrapped here + (the same unwrap PyAutoFit performs when dispatching combined + visualization). + """ + return getattr(self.analysis, "analysis", self.analysis) + + @property + def clocker(self): + return self.analysis_unwrapped.clocker + + +class ResultDataset(Result): + @property + def max_log_likelihood_fit(self): + return self.analysis_unwrapped.fit_via_instance_from(instance=self.instance) + + @property + def mask(self): + return self.max_log_likelihood_fit.mask diff --git a/autocti/util/plot_utils.py b/autocti/util/plot_utils.py index 94ec270d..ca72c092 100644 --- a/autocti/util/plot_utils.py +++ b/autocti/util/plot_utils.py @@ -128,16 +128,23 @@ def fpr_mask_from(dataset) -> Mask2D: pixel_scales=dataset.pixel_scales, ) + # A layout may legitimately lack prescan / overscan regions (e.g. after the + # dataset is trimmed via `apply_settings`), in which case there is nothing + # to mask for that region. serial_prescan = dataset.layout.extract.serial_prescan.serial_prescan - fpr_mask[ - serial_prescan.y0 : serial_prescan.y1, serial_prescan.x0 : serial_prescan.x1 - ] = True + + if serial_prescan is not None: + fpr_mask[ + serial_prescan.y0 : serial_prescan.y1, serial_prescan.x0 : serial_prescan.x1 + ] = True serial_overscan = dataset.layout.extract.serial_overscan.serial_overscan - fpr_mask[ - serial_overscan.y0 : serial_overscan.y1, - serial_overscan.x0 : serial_overscan.x1, - ] = True + + if serial_overscan is not None: + fpr_mask[ + serial_overscan.y0 : serial_overscan.y1, + serial_overscan.x0 : serial_overscan.x1, + ] = True return fpr_mask