From d709e2f2c7f74193f461096f2f77f7d01a081e08 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Fri, 17 Jul 2026 08:14:21 +0100 Subject: [PATCH] CTI resurrection Phase 1: Plotter -> matplotlib function API (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes the Plotter/PlotterInterface object stack, which targeted the autoarray MatPlot/Visuals/Include API removed from the stack, and rewrites PyAutoCTI visualization on the matplotlib function API used by PyAutoGalaxy: - autocti/util/plot_utils.py: the plot_cti_1d primitive (errorbar/log-y/ zero-line/text annotations) + region title/text/FPR-mask helpers ported from the old abstract Plotter. - Per-domain function modules: dataset_1d/plot/{dataset_1d_plots,fit_plots}.py and charge_injection/plot/{imaging_ci_plots,fit_ci_plots}.py — subplot_*, figure_*, combined *_list subplots and fits_fit writers. - Config-gated orchestrators autocti/model/plotter.py (base) + {dataset_1d,charge_injection}/model/plotter.py (PlotterDataset1D, PlotterImagingCI), driven by the af.Visualizer classes (Phase-0 ImportError quarantine removed). - autocti/plot/__init__.py rebuilt as the function namespace; `from . import plot` restored in autocti/__init__.py. - config/visualize.yaml plots schema simplified to the PyAutoGalaxy-style subplot/figure keys (subplot_dataset, subplot_dataset_regions, data, data_logy, data_binned, subplot_fit, subplot_fit_regions, residual_map, residual_map_logy, fits_fit, fpr_non_uniformity, combined_only). - Plot tests rewritten for the function API (plot_patch modernized to intercept Figure.savefig); conftest collect_ignore quarantine removed. test_autocti: 266 passed, 5 skipped (Phase-2 aggregator ports). Co-Authored-By: Claude Fable 5 --- AGENTS.md | 14 +- autocti/__init__.py | 165 +++-- autocti/charge_injection/model/plotter.py | 314 ++++++++ .../model/plotter_interface.py | 399 ----------- autocti/charge_injection/model/visualizer.py | 66 +- autocti/charge_injection/plot/fit_ci_plots.py | 329 +++++++++ .../charge_injection/plot/fit_ci_plotters.py | 617 ---------------- .../charge_injection/plot/imaging_ci_plots.py | 354 +++++++++ .../plot/imaging_ci_plotters.py | 493 ------------- autocti/config/visualize.yaml | 35 +- autocti/dataset_1d/model/plotter.py | 327 +++++++++ autocti/dataset_1d/model/plotter_interface.py | 307 -------- autocti/dataset_1d/model/visualizer.py | 57 +- autocti/dataset_1d/plot/dataset_1d_plots.py | 169 +++++ .../dataset_1d/plot/dataset_1d_plotters.py | 246 ------- autocti/dataset_1d/plot/fit_plots.py | 213 ++++++ autocti/dataset_1d/plot/fit_plotters.py | 353 --------- autocti/model/plotter.py | 55 ++ autocti/model/plotter_interface.py | 33 - autocti/plot/__init__.py | 103 ++- autocti/plot/abstract_plotters.py | 152 ---- autocti/plot/get_visuals/one_d.py | 24 - autocti/plot/get_visuals/two_d.py | 70 -- autocti/util/plot_utils.py | 254 +++++++ .../model/test_plotter_imaging_ci.py | 109 +++ .../model/test_plotter_interface_ci.py | 134 ---- .../charge_injection/plot}/__init__.py | 0 .../plot/test_fit_ci_plots.py | 92 +++ .../plot/test_fit_ci_plotters.py | 191 ----- .../plot/test_imaging_ci_plots.py | 73 ++ .../plot/test_imaging_ci_plotters.py | 121 ---- test_autocti/config/visualize.yaml | 40 +- test_autocti/conftest.py | 670 +++++++++--------- .../model/test_plotter_dataset_1d.py | 97 +++ .../model/test_plotter_interface_1d.py | 113 --- test_autocti/dataset_1d/plot/__init__.py | 0 .../dataset_1d/plot/test_dataset_1d_plots.py | 61 ++ .../plot/test_dataset_1d_plotters.py | 84 --- .../dataset_1d/plot/test_fit_plots.py | 65 ++ .../dataset_1d/plot/test_fit_plotters.py | 70 -- test_autocti/plot/test_abstract_plotters.py | 30 - 41 files changed, 3023 insertions(+), 4076 deletions(-) create mode 100644 autocti/charge_injection/model/plotter.py delete mode 100644 autocti/charge_injection/model/plotter_interface.py create mode 100644 autocti/charge_injection/plot/fit_ci_plots.py delete mode 100644 autocti/charge_injection/plot/fit_ci_plotters.py create mode 100644 autocti/charge_injection/plot/imaging_ci_plots.py delete mode 100644 autocti/charge_injection/plot/imaging_ci_plotters.py create mode 100644 autocti/dataset_1d/model/plotter.py delete mode 100644 autocti/dataset_1d/model/plotter_interface.py create mode 100644 autocti/dataset_1d/plot/dataset_1d_plots.py delete mode 100644 autocti/dataset_1d/plot/dataset_1d_plotters.py create mode 100644 autocti/dataset_1d/plot/fit_plots.py delete mode 100644 autocti/dataset_1d/plot/fit_plotters.py create mode 100644 autocti/model/plotter.py delete mode 100644 autocti/model/plotter_interface.py delete mode 100644 autocti/plot/abstract_plotters.py delete mode 100644 autocti/plot/get_visuals/one_d.py delete mode 100644 autocti/plot/get_visuals/two_d.py create mode 100644 autocti/util/plot_utils.py create mode 100644 test_autocti/charge_injection/model/test_plotter_imaging_ci.py delete mode 100644 test_autocti/charge_injection/model/test_plotter_interface_ci.py rename {autocti/plot/get_visuals => test_autocti/charge_injection/plot}/__init__.py (100%) create mode 100644 test_autocti/charge_injection/plot/test_fit_ci_plots.py delete mode 100644 test_autocti/charge_injection/plot/test_fit_ci_plotters.py create mode 100644 test_autocti/charge_injection/plot/test_imaging_ci_plots.py delete mode 100644 test_autocti/charge_injection/plot/test_imaging_ci_plotters.py create mode 100644 test_autocti/dataset_1d/model/test_plotter_dataset_1d.py delete mode 100644 test_autocti/dataset_1d/model/test_plotter_interface_1d.py create mode 100644 test_autocti/dataset_1d/plot/__init__.py create mode 100644 test_autocti/dataset_1d/plot/test_dataset_1d_plots.py delete mode 100644 test_autocti/dataset_1d/plot/test_dataset_1d_plotters.py create mode 100644 test_autocti/dataset_1d/plot/test_fit_plots.py delete mode 100644 test_autocti/dataset_1d/plot/test_fit_plotters.py delete mode 100644 test_autocti/plot/test_abstract_plotters.py diff --git a/AGENTS.md b/AGENTS.md index f4ae08ee..11e3a75b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,13 +22,13 @@ stack imports autocti — it is a leaf like PyAutoLens. This repo was unmaintained for ~2 years and is being brought back into the ecosystem via the CTI resurrection epic ([PyAutoCTI#82](https://github.com/PyAutoLabs/PyAutoCTI/issues/82)). Phase 0 -(importable + unit tests green on the current stack) is complete. **The -visualization layer (`autocti/plot/`, `*/plot/*_plotters.py`, -`*/model/plotter_interface.py`) is quarantined**: it still targets the removed -autoarray Plotter API and is rewritten on the matplotlib function API -(mirroring PyAutoGalaxy) in Phase 1. Until then `autocti.plot` is not -importable, `Analysis` visualization no-ops with a logged warning, and the -plot tests are skipped via `test_autocti/conftest.py`. +(importable + unit tests green on the current stack) and Phase 1 (the +visualization layer rewritten on the matplotlib **function API**, mirroring +PyAutoGalaxy: per-domain `plot/*_plots.py` function modules, config-gated +`model/plotter.py` orchestrators, `autocti/util/plot_utils.py` helpers) are +complete. Remaining: Phase 2 autofit sync (5 aggregator tests skipped pending +the `AnalysisFactor`/`FactorGraphModel` port), Phase 3 CI + ecosystem +plumbing, Phase 4 workspace update, Phase 5 workspace_test rebuild + release. ## arcticpy (read before installing) diff --git a/autocti/__init__.py b/autocti/__init__.py index 946ddf58..0b9ce4be 100644 --- a/autocti/__init__.py +++ b/autocti/__init__.py @@ -1,84 +1,81 @@ -from autoconf.dictable import from_dict, from_json, output_to_json, to_dict - -from autoarray.mask.mask_1d import Mask1D -from autoarray.layout.region import Region1D -from autoarray.layout.region import Region2D -from autoarray.structures.arrays.uniform_1d import Array1D -from autoarray.structures.arrays.uniform_2d import Array2D -from autoarray.structures.arrays.irregular import ArrayIrregular -from autoarray.structures.header import Header -from autoarray.dataset import preprocess -from autoarray.dataset.imaging.dataset import Imaging - -from arcticpy import ROE -from arcticpy import ROEChargeInjection -from arcticpy import CCD -from arcticpy import CCDPhase -from arcticpy import TrapInstantCapture -from arcticpy import TrapSlowCapture -from arcticpy import TrapInstantCaptureContinuum -from arcticpy import TrapSlowCaptureContinuum -from arcticpy import PixelBounce - -from .charge_injection.fit import FitImagingCI -from .charge_injection.hyper import HyperCINoiseScalar -from .charge_injection.hyper import HyperCINoiseCollection -from .charge_injection.imaging.imaging import ImagingCI -from .charge_injection.imaging.settings import SettingsImagingCI -from .charge_injection.imaging.simulator import SimulatorImagingCI -from .charge_injection.layout import Layout2DCI -from .cosmics.cosmics import SimulatorCosmicRayMap -from .extract.settings import SettingsExtract -from .extract.two_d.parallel.overscan import Extract2DParallelOverscan -from .extract.two_d.parallel.fpr import Extract2DParallelFPR -from .extract.two_d.parallel.eper import Extract2DParallelEPER -from .extract.two_d.parallel.pre_injection import Extract2DParallelPreInjection -from .extract.two_d.parallel.pedestal import Extract2DParallelPedestal -from .extract.two_d.serial.prescan import Extract2DSerialPrescan -from .extract.two_d.serial.overscan import Extract2DSerialOverscan -from .extract.two_d.serial.overscan_no_eper import Extract2DSerialOverscanNoEPER -from .extract.two_d.serial.fpr import Extract2DSerialFPR -from .extract.two_d.serial.eper import Extract2DSerialEPER -from .extract.two_d.parallel.calibration import Extract2DParallelCalibration -from .extract.two_d.serial.calibration import Extract2DSerialCalibration -from .extract.two_d.master import Extract2DMaster -from .instruments import euclid -from .instruments import acs -from .charge_injection.fit import FitImagingCI -from .charge_injection.imaging.readout_persistence import ReadoutPersistence -from .mask.mask_2d import Mask2D -from .mask.mask_2d import SettingsMask2D -from .mask.mask_1d import Mask1D -from .mask.mask_1d import SettingsMask1D -from .extract.one_d.overscan import Extract1DOverscan -from .extract.one_d.fpr import Extract1DFPR -from .extract.one_d.eper import Extract1DEPER -from .extract.one_d.master import Extract1DMaster -from .layout.one_d import Layout1D -from .dataset_1d.dataset_1d.dataset_1d import Dataset1D -from .dataset_1d.dataset_1d.simulator import SimulatorDataset1D -from .dataset_1d.fit import FitDataset1D -from .dataset_1d.model.analysis import AnalysisDataset1D -from .dataset_1d.model.result import ResultDataset1D -from .charge_injection.model.analysis import AnalysisImagingCI -from .charge_injection.model.result import ResultImagingCI -from .model.analysis import AnalysisCTI -from .model.model_util import CTI1D -from .model.model_util import CTI2D -from .model.settings import SettingsCTI1D -from .model.settings import SettingsCTI2D -from .clocker.one_d import Clocker1D -from .clocker.two_d import Clocker2D -from . import aggregator as agg -from . import util - -# `from . import plot` is quarantined: the Plotter object stack targets the -# removed autoarray Plotter API and is rewritten on the new matplotlib function -# API in Phase 1 of the CTI resurrection epic (PyAutoCTI#82). -from . import mock as m # noqa - -from autoconf import conf - -conf.instance.register(__file__) - -__version__ = "2024.11.13.2" +from autoconf.dictable import from_dict, from_json, output_to_json, to_dict + +from autoarray.mask.mask_1d import Mask1D +from autoarray.layout.region import Region1D +from autoarray.layout.region import Region2D +from autoarray.structures.arrays.uniform_1d import Array1D +from autoarray.structures.arrays.uniform_2d import Array2D +from autoarray.structures.arrays.irregular import ArrayIrregular +from autoarray.structures.header import Header +from autoarray.dataset import preprocess +from autoarray.dataset.imaging.dataset import Imaging + +from arcticpy import ROE +from arcticpy import ROEChargeInjection +from arcticpy import CCD +from arcticpy import CCDPhase +from arcticpy import TrapInstantCapture +from arcticpy import TrapSlowCapture +from arcticpy import TrapInstantCaptureContinuum +from arcticpy import TrapSlowCaptureContinuum +from arcticpy import PixelBounce + +from .charge_injection.fit import FitImagingCI +from .charge_injection.hyper import HyperCINoiseScalar +from .charge_injection.hyper import HyperCINoiseCollection +from .charge_injection.imaging.imaging import ImagingCI +from .charge_injection.imaging.settings import SettingsImagingCI +from .charge_injection.imaging.simulator import SimulatorImagingCI +from .charge_injection.layout import Layout2DCI +from .cosmics.cosmics import SimulatorCosmicRayMap +from .extract.settings import SettingsExtract +from .extract.two_d.parallel.overscan import Extract2DParallelOverscan +from .extract.two_d.parallel.fpr import Extract2DParallelFPR +from .extract.two_d.parallel.eper import Extract2DParallelEPER +from .extract.two_d.parallel.pre_injection import Extract2DParallelPreInjection +from .extract.two_d.parallel.pedestal import Extract2DParallelPedestal +from .extract.two_d.serial.prescan import Extract2DSerialPrescan +from .extract.two_d.serial.overscan import Extract2DSerialOverscan +from .extract.two_d.serial.overscan_no_eper import Extract2DSerialOverscanNoEPER +from .extract.two_d.serial.fpr import Extract2DSerialFPR +from .extract.two_d.serial.eper import Extract2DSerialEPER +from .extract.two_d.parallel.calibration import Extract2DParallelCalibration +from .extract.two_d.serial.calibration import Extract2DSerialCalibration +from .extract.two_d.master import Extract2DMaster +from .instruments import euclid +from .instruments import acs +from .charge_injection.fit import FitImagingCI +from .charge_injection.imaging.readout_persistence import ReadoutPersistence +from .mask.mask_2d import Mask2D +from .mask.mask_2d import SettingsMask2D +from .mask.mask_1d import Mask1D +from .mask.mask_1d import SettingsMask1D +from .extract.one_d.overscan import Extract1DOverscan +from .extract.one_d.fpr import Extract1DFPR +from .extract.one_d.eper import Extract1DEPER +from .extract.one_d.master import Extract1DMaster +from .layout.one_d import Layout1D +from .dataset_1d.dataset_1d.dataset_1d import Dataset1D +from .dataset_1d.dataset_1d.simulator import SimulatorDataset1D +from .dataset_1d.fit import FitDataset1D +from .dataset_1d.model.analysis import AnalysisDataset1D +from .dataset_1d.model.result import ResultDataset1D +from .charge_injection.model.analysis import AnalysisImagingCI +from .charge_injection.model.result import ResultImagingCI +from .model.analysis import AnalysisCTI +from .model.model_util import CTI1D +from .model.model_util import CTI2D +from .model.settings import SettingsCTI1D +from .model.settings import SettingsCTI2D +from .clocker.one_d import Clocker1D +from .clocker.two_d import Clocker2D +from . import aggregator as agg +from . import util +from . import plot +from . import mock as m # noqa + +from autoconf import conf + +conf.instance.register(__file__) + +__version__ = "2024.11.13.2" diff --git a/autocti/charge_injection/model/plotter.py b/autocti/charge_injection/model/plotter.py new file mode 100644 index 00000000..41023ee2 --- /dev/null +++ b/autocti/charge_injection/model/plotter.py @@ -0,0 +1,314 @@ +import logging +from typing import List + +from autocti.charge_injection.imaging.imaging import ImagingCI +from autocti.charge_injection.fit import FitImagingCI +from autocti.charge_injection.plot import imaging_ci_plots +from autocti.charge_injection.plot import fit_ci_plots +from autocti.model.plotter import Plotter, plot_setting + +from autocti import exc + +logger = logging.getLogger(__name__) + + +class PlotterImagingCI(Plotter): + """ + Outputs visualization of ``ImagingCI`` datasets and fits during a model fit, using the matplotlib + function API in ``autocti.charge_injection.plot``. + + The images output are customized via the ``plots`` section of ``config/visualize.yaml``. + """ + + def dataset(self, dataset: ImagingCI, folder_suffix: str = ""): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset{folder_suffix}" + + if should_plot("subplot_dataset"): + imaging_ci_plots.subplot_dataset( + dataset=dataset, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + def dataset_regions( + self, dataset: ImagingCI, region_list: List[str], folder_suffix: str = "" + ): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset{folder_suffix}" + + for region in region_list: + try: + if should_plot("subplot_dataset_regions"): + imaging_ci_plots.subplot_dataset_region( + dataset=dataset, + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data"): + imaging_ci_plots.figure_data_region( + dataset=dataset, + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + imaging_ci_plots.figure_data_region( + dataset=dataset, + region=region, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" + ) + + if should_plot("data_binned"): + try: + imaging_ci_plots.subplot_data_binned( + dataset=dataset, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + except (exc.PlottingException, exc.RegionException, TypeError, ValueError): + logger.info( + "VISUALIZATION - Could not visualize the ImagingCI binned data" + ) + + def dataset_combined( + self, + dataset_list: List[ImagingCI], + folder_suffix: str = "", + filename_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset_combined{folder_suffix}" + + if should_plot("subplot_dataset"): + imaging_ci_plots.subplot_dataset_list( + dataset_list=dataset_list, + output_path=output_path, + output_filename=f"subplot_dataset_list{filename_suffix}", + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + def dataset_regions_combined( + self, + dataset_list: List[ImagingCI], + region_list: List[str], + folder_suffix: str = "", + filename_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset_combined{folder_suffix}" + + for region in region_list: + try: + if should_plot("data"): + imaging_ci_plots.subplot_data_region_list( + dataset_list=dataset_list, + region=region, + output_path=output_path, + output_filename=f"subplot_data_list{filename_suffix}_{region}", + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + imaging_ci_plots.subplot_data_region_list( + dataset_list=dataset_list, + region=region, + logy=True, + output_path=output_path, + output_filename=f"subplot_data_logy_list{filename_suffix}_{region}", + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" + ) + + def fit(self, fit: FitImagingCI, during_analysis: bool, folder_suffix: str = ""): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset{folder_suffix}" + + if should_plot("subplot_fit"): + fit_ci_plots.subplot_fit( + fit=fit, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if not during_analysis: + if should_plot("fits_fit"): + fit_ci_plots.fits_fit(fit=fit, output_path=output_path) + + def fit_1d_regions( + self, + fit: FitImagingCI, + region_list: List[str], + during_analysis: bool, + folder_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset{folder_suffix}" + + for region in region_list: + try: + if should_plot("subplot_fit_regions"): + fit_ci_plots.subplot_fit_region( + fit=fit, + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data"): + fit_ci_plots.figure_fit_region( + fit=fit, + quantity="data", + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + fit_ci_plots.figure_fit_region( + fit=fit, + quantity="data", + region=region, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("residual_map"): + fit_ci_plots.figure_fit_region( + fit=fit, + quantity="residual_map", + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("residual_map_logy"): + fit_ci_plots.figure_fit_region( + fit=fit, + quantity="residual_map", + region=region, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" + ) + + def fit_combined( + self, + fit_list: List[FitImagingCI], + during_analysis: bool, + folder_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset_combined{folder_suffix}" + + if should_plot("residual_map"): + for quantity in [ + "residual_map", + "normalized_residual_map", + "chi_squared_map", + ]: + fit_ci_plots.subplot_fit_list( + fit_list=fit_list, + quantity=quantity, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + def fit_1d_regions_combined( + self, + fit_list: List[FitImagingCI], + region_list: List[str], + during_analysis: bool, + folder_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset_combined{folder_suffix}" + + for region in region_list: + try: + if should_plot("data"): + fit_ci_plots.subplot_fit_region_list( + fit_list=fit_list, + region=region, + quantity="data", + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + fit_ci_plots.subplot_fit_region_list( + fit_list=fit_list, + region=region, + quantity="data", + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("residual_map"): + fit_ci_plots.subplot_fit_region_list( + fit_list=fit_list, + region=region, + quantity="residual_map", + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" + ) diff --git a/autocti/charge_injection/model/plotter_interface.py b/autocti/charge_injection/model/plotter_interface.py deleted file mode 100644 index f51d0986..00000000 --- a/autocti/charge_injection/model/plotter_interface.py +++ /dev/null @@ -1,399 +0,0 @@ -import logging - -import autoarray.plot as aplt - -import autocti.plot as aplt - -from autocti.model.plotter_interface import PlotterInterface -from autocti.model.plotter_interface import plot_setting - -from autocti import exc - -logger = logging.getLogger(__name__) - -logger.setLevel(level="INFO") - - -class PlotterInterfaceImagingCI(PlotterInterface): - def dataset(self, dataset, folder_suffix: str = ""): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot = self.mat_plot_2d_from(subfolders=f"dataset{folder_suffix}") - - dataset_plotter = aplt.ImagingCIPlotter( - dataset=dataset, mat_plot_2d=mat_plot, include_2d=self.include_2d - ) - - if should_plot("subplot_dataset"): - dataset_plotter.subplot_dataset() - - dataset_plotter.figures_2d( - data=should_plot("data"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - cosmic_ray_map=should_plot("cosmic_ray_map"), - ) - - def dataset_regions(self, dataset, region_list, folder_suffix: str = ""): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot = self.mat_plot_1d_from(subfolders=f"dataset{folder_suffix}") - - dataset_plotter = aplt.ImagingCIPlotter( - dataset=dataset, mat_plot_1d=mat_plot, include_2d=self.include_2d - ) - - for region in region_list: - try: - if should_plot("subplot_dataset"): - dataset_plotter.subplot_1d(region=region) - - dataset_plotter.figures_1d( - region=region, - data=should_plot("data"), - data_logy=should_plot("data_logy"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - ) - - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" - ) - - def dataset_combined( - self, dataset_list, folder_suffix: str = "", filename_suffix: str = "" - ): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot = self.mat_plot_2d_from(subfolders=f"dataset_combined{folder_suffix}") - - dataset_plotter_list = [ - aplt.ImagingCIPlotter( - dataset=dataset, mat_plot_2d=mat_plot, include_2d=self.include_2d - ) - for dataset in dataset_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=dataset_plotter_list) - - if should_plot("subplot_dataset"): - multi_plotter.subplot_of_figure( - func_name="figures_2d", - figure_name="data", - filename_suffix=filename_suffix, - ) - multi_plotter.subplot_of_figure( - func_name="figures_2d", - figure_name="noise_map", - filename_suffix=filename_suffix, - ) - multi_plotter.subplot_of_figure( - func_name="figures_2d", - figure_name="signal_to_noise_map", - filename_suffix=filename_suffix, - ) - multi_plotter.subplot_of_figure( - func_name="figures_2d", - figure_name="pre_cti_data", - filename_suffix=filename_suffix, - ) - multi_plotter.subplot_of_figure( - func_name="figures_2d", - figure_name="pre_cti_data_residual_map", - filename_suffix=filename_suffix, - ) - multi_plotter.subplot_of_figure( - func_name="figures_2d", - figure_name="cosmic_ray_map", - filename_suffix=filename_suffix, - ) - - def dataset_regions_combined( - self, - dataset_list, - region_list, - folder_suffix: str = "", - filename_suffix: str = "", - ): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot_1d = self.mat_plot_1d_from( - subfolders=f"dataset_combined{folder_suffix}" - ) - - dataset_plotter_list = [ - aplt.ImagingCIPlotter( - dataset=dataset, mat_plot_2d=mat_plot_1d, include_1d=self.include_1d - ) - for dataset in dataset_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=dataset_plotter_list) - - for region in region_list: - try: - if should_plot("data"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data", - region=region, - filename_suffix=f"{filename_suffix}_{region}", - ) - - if should_plot("data_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data_logy", - region=region, - filename_suffix=f"{filename_suffix}_{region}", - ) - - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" - ) - - if folder_suffix == "_full": - for figure_name in [ - "rows_fpr", - "rows_no_fpr", - "columns_fpr", - "columns_no_fpr", - ]: - if should_plot(figure_name): - multi_plotter.subplot_of_figure( - func_name="figures_1d_data_binned", - figure_name=figure_name, - filename_suffix=f"{filename_suffix}", - ) - - def fit(self, fit, during_analysis, folder_suffix: str = ""): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot = self.mat_plot_2d_from(subfolders=f"fit_dataset{folder_suffix}") - - fit_plotter = aplt.FitImagingCIPlotter( - fit=fit, mat_plot_2d=mat_plot, include_2d=self.include_2d - ) - - fit_plotter.figures_2d( - data=should_plot("data"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - post_cti_data=should_plot("post_cti_data"), - residual_map=should_plot("residual_map"), - normalized_residual_map=should_plot("normalized_residual_map"), - chi_squared_map=should_plot("chi_squared_map"), - ) - - if not during_analysis: - if should_plot("all_at_end_png"): - fit_plotter.figures_2d( - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - residual_map=True, - normalized_residual_map=True, - chi_squared_map=True, - ) - - if should_plot("all_at_end_fits"): - self.fit_in_fits(fit=fit) - - if should_plot("subplot_fit"): - fit_plotter.subplot_fit() - - def fit_1d_regions( - self, fit, region_list, during_analysis, folder_suffix: str = "" - ): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot = self.mat_plot_1d_from(subfolders=f"fit_dataset{folder_suffix}") - - fit_plotter = aplt.FitImagingCIPlotter( - fit=fit, mat_plot_1d=mat_plot, include_1d=self.include_1d - ) - - for region in region_list: - try: - if should_plot("subplot_fit"): - fit_plotter.subplot_1d(region=region) - - fit_plotter.figures_1d( - region=region, - data=should_plot("data"), - data_logy=should_plot("data_logy"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - post_cti_data=should_plot("post_cti_data"), - residual_map=should_plot("residual_map"), - residual_map_logy=should_plot("residual_map_logy"), - normalized_residual_map=should_plot("normalized_residual_map"), - chi_squared_map=should_plot("chi_squared_map"), - ) - - if not during_analysis: - if should_plot("all_at_end_png"): - fit_plotter.figures_1d( - region=region, - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - residual_map=True, - residual_map_logy=True, - normalized_residual_map=True, - chi_squared_map=True, - ) - - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" - ) - - def fit_combined(self, fit_list, during_analysis, folder_suffix: str = ""): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot = self.mat_plot_2d_from( - subfolders=f"fit_dataset_combined{folder_suffix}" - ) - - fit_plotter_list = [ - aplt.FitImagingCIPlotter( - fit=fit, mat_plot_2d=mat_plot, include_2d=self.include_2d - ) - for fit in fit_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=fit_plotter_list) - - if should_plot("residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_2d", figure_name="residual_map" - ) - - if should_plot("normalized_residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_2d", figure_name="normalized_residual_map" - ) - - if should_plot("chi_squared_map"): - multi_plotter.subplot_of_figure( - func_name="figures_2d", figure_name="chi_squared_map" - ) - - def fit_1d_regions_combined( - self, fit_list, region_list, during_analysis, folder_suffix: str = "" - ): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot = self.mat_plot_1d_from( - subfolders=f"fit_dataset_combined{folder_suffix}" - ) - - fit_plotter_list = [ - aplt.FitImagingCIPlotter( - fit=fit, mat_plot_1d=mat_plot, include_1d=self.include_1d - ) - for fit in fit_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=fit_plotter_list) - - for region in region_list: - try: - if should_plot("data"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("data_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data_logy", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="residual_map", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("residual_map_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="residual_map_logy", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("normalized_residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="normalized_residual_map", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("chi_squared_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="chi_squared_map", - region=region, - filename_suffix=f"_{region}", - ) - - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the ImagingCI 1D {region}" - ) - - if folder_suffix == "_full": - for figure_name in [ - "rows_fpr", - "rows_no_fpr", - "columns_fpr", - "columns_no_fpr", - ]: - if should_plot(figure_name): - multi_plotter.subplot_of_figure( - func_name="figures_1d_data_binned", - figure_name=figure_name, - ) - - def fit_in_fits(self, fit): - mat_plot_2d = self.mat_plot_2d_from(subfolders="fit_imaging/fit", format="fit") - - fit_plotter = aplt.FitImagingCIPlotter( - fit=fit, mat_plot_2d=mat_plot_2d, include_2d=self.include_2d - ) - - fit_plotter.figures_2d( - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - residual_map=True, - normalized_residual_map=True, - chi_squared_map=True, - ) diff --git a/autocti/charge_injection/model/visualizer.py b/autocti/charge_injection/model/visualizer.py index 0cee02e1..bb97f907 100644 --- a/autocti/charge_injection/model/visualizer.py +++ b/autocti/charge_injection/model/visualizer.py @@ -4,6 +4,8 @@ import autofit as af +from autocti.charge_injection.model.plotter import PlotterImagingCI + logger = logging.getLogger(__name__) @@ -28,25 +30,11 @@ def visualize_before_fit( The model object, which includes model components representing the galaxies that are fitted to the imaging data. """ - # Imported lazily: the PlotterInterface stack still targets the removed - # autoarray Plotter API and is rewritten on the new matplotlib function - # API in Phase 1 of the CTI resurrection epic (PyAutoCTI#82). - try: - from autocti.charge_injection.model.plotter_interface import ( - PlotterInterfaceImagingCI, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - if conf.instance["visualize"]["plots"]["combined_only"]: return - visualizer = PlotterInterfaceImagingCI(image_path=paths.image_path) + visualizer = PlotterImagingCI(image_path=paths.image_path) region_list = analysis.region_list_from(model=model) @@ -70,24 +58,10 @@ def visualize_before_fit_combined( paths: af.AbstractPaths, model: af.AbstractPriorModel, ): - # Imported lazily: the PlotterInterface stack still targets the removed - # autoarray Plotter API and is rewritten on the new matplotlib function - # API in Phase 1 of the CTI resurrection epic (PyAutoCTI#82). - try: - from autocti.charge_injection.model.plotter_interface import ( - PlotterInterfaceImagingCI, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - if analyses is None: return - visualizer = PlotterInterfaceImagingCI(image_path=paths.image_path) + visualizer = PlotterImagingCI(image_path=paths.image_path) region_list = analyses[0].region_list_from(model=model) @@ -157,20 +131,6 @@ def visualize( If True the visualization is being performed midway through the non-linear search before it is finished, which may change which images are output. """ - # Imported lazily: the PlotterInterface stack still targets the removed - # autoarray Plotter API and is rewritten on the new matplotlib function - # API in Phase 1 of the CTI resurrection epic (PyAutoCTI#82). - try: - from autocti.charge_injection.model.plotter_interface import ( - PlotterInterfaceImagingCI, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - if conf.instance["visualize"]["plots"]["combined_only"]: return @@ -178,7 +138,7 @@ def visualize( fit = analysis.fit_via_instance_from(instance=instance) region_list = analysis.region_list_from(model=instance) - visualizer = PlotterInterfaceImagingCI(image_path=paths.image_path) + visualizer = PlotterImagingCI(image_path=paths.image_path) visualizer.fit(fit=fit, during_analysis=during_analysis) visualizer.fit_1d_regions( fit=fit, during_analysis=during_analysis, region_list=region_list @@ -206,20 +166,6 @@ def visualize_combined( instance: af.ModelInstance, during_analysis: bool, ): - # Imported lazily: the PlotterInterface stack still targets the removed - # autoarray Plotter API and is rewritten on the new matplotlib function - # API in Phase 1 of the CTI resurrection epic (PyAutoCTI#82). - try: - from autocti.charge_injection.model.plotter_interface import ( - PlotterInterfaceImagingCI, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - if analyses is None: return @@ -236,7 +182,7 @@ def visualize_combined( region_list = analyses[0].region_list_from(model=instance) - visualizer = PlotterInterfaceImagingCI(image_path=paths.image_path) + visualizer = PlotterImagingCI(image_path=paths.image_path) visualizer.fit_combined(fit_list=fit_list, during_analysis=during_analysis) visualizer.fit_1d_regions_combined( fit_list=fit_list, diff --git a/autocti/charge_injection/plot/fit_ci_plots.py b/autocti/charge_injection/plot/fit_ci_plots.py new file mode 100644 index 00000000..82a34a70 --- /dev/null +++ b/autocti/charge_injection/plot/fit_ci_plots.py @@ -0,0 +1,329 @@ +from pathlib import Path +from typing import List, Optional + +import numpy as np + +from autoconf.fitsable import hdu_list_for_output_from +from autoarray.plot.array import plot_array +from autoarray.plot.utils import ( + subplots, + subplot_save, + conf_subplot_figsize, + tight_layout, +) + +from autocti.charge_injection.fit import FitImagingCI +from autocti.util import plot_utils + +# The fit quantities plottable in 2D and (binned over a region) in 1D. +_QUANTITY_TITLES = { + "data": "Data", + "noise_map": "Noise Map", + "signal_to_noise_map": "Signal To Noise Map", + "pre_cti_data": "Pre CTI Data", + "post_cti_data": "Post CTI Data", + "residual_map": "Residual Map", + "normalized_residual_map": "Normalized Residual Map", + "chi_squared_map": "Chi-Squared Map", +} + + +def _extract(fit: FitImagingCI, array, region: str): + return fit.dataset.layout.extract_region_from(array=array, region=region) + + +def figure_fit_region( + fit: FitImagingCI, + quantity: str, + region: str, + logy: bool = False, + ax=None, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Plot a quantity of a ``FitImagingCI`` extracted and binned over a region (e.g. the parallel FPR) as a + 1D figure. + + The ``data`` quantity is drawn as an errorbar figure with the binned model data overlaid in red; all + other quantities are plain line figures. + + Parameters + ---------- + fit + The charge injection fit whose quantity is plotted. + quantity + One of {"data", "noise_map", "signal_to_noise_map", "pre_cti_data", "post_cti_data", + "residual_map", "normalized_residual_map", "chi_squared_map"}. + region + The region extracted and binned {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"}. + logy + Whether the y-axis is log10 scaled. + """ + title_str = plot_utils.title_str_from(dataset=fit.dataset, region=region) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + is_data = quantity == "data" + + plot_utils.plot_cti_1d( + y=_extract(fit, getattr(fit, quantity), region), + y_errors=_extract(fit, fit.noise_map, region) if is_data else None, + y_extra=_extract(fit, fit.model_data, region) if is_data else None, + errorbar=is_data, + logy=logy, + should_plot_zero=plot_utils.should_plot_zero_from(region=region), + text_manual_dict=( + plot_utils.text_manual_dict_from(dataset=fit.dataset, region=region) + if is_data + else None + ), + text_manual_dict_y=plot_utils.text_manual_dict_y_from(region=region), + title=_pf( + f"{_QUANTITY_TITLES[quantity]} {title_str}" + (" [log10 y]" if logy else "") + ), + ylabel="e-" if quantity != "signal_to_noise_map" else "", + ax=ax, + output_path=output_path, + output_filename=f"{quantity}_logy_{region}" if logy else f"{quantity}_{region}", + output_format=output_format, + ) + + +def subplot_fit( + fit: FitImagingCI, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Eight-panel 2D subplot of a ``FitImagingCI``: the data, noise map, signal-to-noise map, pre-CTI data, + post-CTI data, residual map, normalized residual map and chi-squared map. + + Parameters + ---------- + fit + The charge injection fit to visualize. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + title_str = plot_utils.title_str_2d_from(dataset=fit.dataset) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + fig, axes = subplots(2, 4, figsize=conf_subplot_figsize(2, 4)) + axes = axes.flatten() + + for i, (quantity, title) in enumerate(_QUANTITY_TITLES.items()): + plot_array( + getattr(fit, quantity), + ax=axes[i], + title=_pf(title if title_str is None else f"{title_str} {title}"), + ) + + tight_layout() + subplot_save(fig, output_path, "subplot_fit", output_format) + + +def subplot_fit_region( + fit: FitImagingCI, + region: str, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Four-panel 1D subplot of a ``FitImagingCI`` extracted and binned over a region (e.g. the parallel + FPR): the data (with model overlay), residual map, normalized residual map and chi-squared map. + + Parameters + ---------- + fit + The charge injection fit to visualize. + region + The region extracted and binned {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"}. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + fig, axes = subplots(2, 2, figsize=conf_subplot_figsize(2, 2)) + axes = axes.flatten() + + for i, quantity in enumerate( + ["data", "residual_map", "normalized_residual_map", "chi_squared_map"] + ): + figure_fit_region( + fit=fit, + quantity=quantity, + region=region, + ax=axes[i], + title_prefix=title_prefix, + ) + + tight_layout() + subplot_save(fig, output_path, f"subplot_1d_fit_ci_{region}", output_format) + + +def subplot_noise_scaling_map_dict( + fit: FitImagingCI, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Subplot of every noise scaling map of a ``FitImagingCI``, which are used by hyper-functionality to + scale the noise map between fits. + + Parameters + ---------- + fit + The charge injection fit whose noise scaling maps are visualized. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + n = len(fit.noise_scaling_map_dict) + cols = min(n, 2) + rows = (n + cols - 1) // cols + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.flatten() if hasattr(axes, "flatten") else [axes] + + for i, (key, noise_scaling_map) in enumerate(fit.noise_scaling_map_dict.items()): + plot_array(noise_scaling_map, ax=axes[i], title=_pf(f"Noise Scaling Map {key}")) + + for ax in axes[n:]: + ax.axis("off") + + tight_layout() + subplot_save(fig, output_path, "subplot_noise_scaling_map_dict", output_format) + + +def subplot_fit_list( + fit_list: List[FitImagingCI], + quantity: str = "residual_map", + output_path=None, + output_filename: str = None, + output_format=None, + title_prefix: str = None, +): + """ + Combined 2D subplot of one quantity of a list of ``FitImagingCI`` objects (e.g. from a combined + analysis), one panel per fit. + + Parameters + ---------- + fit_list + The list of charge injection fits to visualize. + quantity + The fit quantity plotted in every panel (see ``figure_fit_region``). + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + n = len(fit_list) + cols = min(n, 3) + rows = (n + cols - 1) // cols + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.flatten() if hasattr(axes, "flatten") else [axes] + + for i, fit in enumerate(fit_list): + plot_array( + getattr(fit, quantity), + ax=axes[i], + title=_pf(f"{_QUANTITY_TITLES[quantity]} {i}"), + ) + + for ax in axes[n:]: + ax.axis("off") + + tight_layout() + + if output_filename is None: + output_filename = f"subplot_{quantity}_list" + + subplot_save(fig, output_path, output_filename, output_format) + + +def subplot_fit_region_list( + fit_list: List[FitImagingCI], + region: str, + quantity: str = "data", + logy: bool = False, + output_path=None, + output_filename: str = None, + output_format=None, + title_prefix: str = None, +): + """ + Combined 1D subplot of one binned quantity of a list of ``FitImagingCI`` objects over a region (e.g. + the parallel EPER), one panel per fit. + + Parameters + ---------- + fit_list + The list of charge injection fits to visualize. + region + The region extracted and binned {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"}. + quantity + The fit quantity plotted in every panel (see ``figure_fit_region``). + logy + Whether the y-axes are log10 scaled. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + n = len(fit_list) + cols = min(n, 3) + rows = (n + cols - 1) // cols + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.flatten() if hasattr(axes, "flatten") else [axes] + + for i, fit in enumerate(fit_list): + figure_fit_region( + fit=fit, + quantity=quantity, + region=region, + logy=logy, + ax=axes[i], + title_prefix=title_prefix, + ) + + for ax in axes[n:]: + ax.axis("off") + + tight_layout() + + if output_filename is None: + output_filename = f"subplot_{quantity}{'_logy' if logy else ''}_list_{region}" + + subplot_save(fig, output_path, output_filename, output_format) + + +def fits_fit(fit: FitImagingCI, output_path): + """ + Output the quantities of a ``FitImagingCI`` to a single ``fit.fits`` file with one extension per + quantity (post-CTI data, residual map, normalized residual map and chi-squared map). + """ + hdu_list = hdu_list_for_output_from( + values_list=[ + np.asarray(fit.post_cti_data.native), + np.asarray(fit.residual_map.native), + np.asarray(fit.normalized_residual_map.native), + np.asarray(fit.chi_squared_map.native), + ], + ext_name_list=[ + "post_cti_data", + "residual_map", + "normalized_residual_map", + "chi_squared_map", + ], + header_dict=fit.dataset.mask.header_dict, + ) + hdu_list.writeto(Path(output_path) / "fit.fits", overwrite=True) diff --git a/autocti/charge_injection/plot/fit_ci_plotters.py b/autocti/charge_injection/plot/fit_ci_plotters.py deleted file mode 100644 index 19d3d8a0..00000000 --- a/autocti/charge_injection/plot/fit_ci_plotters.py +++ /dev/null @@ -1,617 +0,0 @@ -import copy -import numpy as np -from typing import Callable - -import autoarray.plot as aplt - -from autoarray.fit.plot.fit_imaging_plotters import FitImagingPlotterMeta -from autoarray.plot.auto_labels import AutoLabels - -from autocti.plot.abstract_plotters import Plotter -from autocti.charge_injection.fit import FitImagingCI -from autocti.extract.settings import SettingsExtract - -from autocti import exc - - -class FitImagingCIPlotter(Plotter): - def __init__( - self, - fit: FitImagingCI, - mat_plot_2d: aplt.MatPlot2D = aplt.MatPlot2D(), - visuals_2d: aplt.Visuals2D = aplt.Visuals2D(), - include_2d: aplt.Include2D = aplt.Include2D(), - mat_plot_1d: aplt.MatPlot1D = aplt.MatPlot1D(), - visuals_1d: aplt.Visuals1D = aplt.Visuals1D(), - include_1d: aplt.Include1D = aplt.Include1D(), - residuals_symmetric_cmap: bool = True, - ): - """ - Plots the attributes of `FitImagingCI` objects using the matplotlib methods `imshow()`, `plot()` and many other - matplotlib functions which customize the plot's appearance. - - The `mat_plot_1d` and `mat_plot_2d` attribute wraps matplotlib function calls to make the figure. By default, - the settings passed to every matplotlib function called are those specified in - the `config/visualize/mat_wrap/*.ini` files, but a user can manually input values into `MatPlot1D` and - `MatPlot2D` to customize the figure's appearance. - - Overlaid on the figure are visuals, contained in the `Visuals1D` and `Visuals2D` object. Attributes may be - extracted from the `FitImagingCI` and plotted via the visuals object, if the corresponding entry - is `True` in the `Include1D` and `Include2D` object or the `config/visualize/include.ini` file. - - Parameters - ---------- - fit - The fit to an imaging dataset the plotter plots. - get_visuals_2d - A function which extracts from the `FitImaging` the 2D visuals which are plotted on figures. - mat_plot_2d - Contains objects which wrap the matplotlib function calls that make the plot. - visuals_2d - Contains visuals that can be overlaid on the plot. - include_2d - Specifies which attributes of the `Array2D` are extracted and plotted as visuals. - mat_plot_1d - Contains objects which wrap the matplotlib function calls that make 1D plots. - visuals_1d - Contains 1D visuals that can be overlaid on 1D plots. - include_1d - Specifies which attributes of the `ImagingCI` are extracted and plotted as visuals for 1D plots. - residuals_symmetric_cmap - If true, the `residual_map` and `normalized_residual_map` are plotted with a symmetric color map such - that `abs(vmin) = abs(vmax)`. - """ - super().__init__( - dataset=fit.dataset, - mat_plot_2d=mat_plot_2d, - include_2d=include_2d, - visuals_2d=visuals_2d, - ) - - self.visuals_1d = visuals_1d - self.include_1d = include_1d - self.mat_plot_1d = mat_plot_1d - - self.fit = fit - - self._fit_imaging_meta_plotter = FitImagingPlotterMeta( - fit=self.fit, - get_visuals_2d=self.get_visuals_2d, - mat_plot_2d=self.mat_plot_2d, - include_2d=self.include_2d, - visuals_2d=self.visuals_2d, - residuals_symmetric_cmap=residuals_symmetric_cmap, - ) - - self.residuals_symmetric_cmap = residuals_symmetric_cmap - - def get_visuals_2d(self): - return self.get_2d.via_fit_imaging_ci_from(fit=self.fit) - - @property - def extract_region_from(self) -> Callable: - return self.fit.dataset.layout.extract_region_from - - @property - def extract_region_noise_map_from(self) -> Callable: - return self.fit.dataset.layout.extract_region_noise_map_from - - def figures_2d( - self, - data: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - post_cti_data: bool = False, - residual_map: bool = False, - normalized_residual_map: bool = False, - chi_squared_map: bool = False, - ): - """ - Plots the individual attributes of the plotter's `FitImagingCI` object in 2D. - - The API is such that every plottable attribute of the `FitImagingCI` object is an input parameter of type bool - of the function, which if switched to `True` means that it is plotted. - - Parameters - ---------- - data - Whether to make a 2D plot (via `imshow`) of the image data. - noise_map - Whether to make a 2D plot (via `imshow`) of the noise map. - signal_to_noise_map - Whether to make a 2D plot (via `imshow`) of the signal-to-noise map. - pre_cti_data - Whether to make a 2D plot (via `imshow`) of the pre-cti data. - post_cti_data - Whether to make a 2D plot (via `imshow`) of the post-cti data. - residual_map - Whether to make a 2D plot (via `imshow`) of the residual map. - normalized_residual_map - Whether to make a 2D plot (via `imshow`) of the normalized residual map. - chi_squared_map - Whether to make a 2D plot (via `imshow`) of the chi-squared map. - """ - self._fit_imaging_meta_plotter.figures_2d( - data=data, - noise_map=noise_map, - signal_to_noise_map=signal_to_noise_map, - residual_map=residual_map, - normalized_residual_map=normalized_residual_map, - chi_squared_map=chi_squared_map, - ) - - if pre_cti_data: - self.mat_plot_2d.plot_array( - array=self.fit.pre_cti_data, - visuals_2d=self.get_visuals_2d(), - auto_labels=AutoLabels(title="Pre CTI Data", filename="pre_cti_data"), - ) - - if post_cti_data: - self.mat_plot_2d.plot_array( - array=self.fit.post_cti_data, - visuals_2d=self.get_visuals_2d(), - auto_labels=AutoLabels( - title="CI Post CTI Image", filename="post_cti_data" - ), - ) - - def figures_1d( - self, - region, - data: bool = False, - data_logy: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - post_cti_data: bool = False, - residual_map: bool = False, - residual_map_logy: bool = False, - normalized_residual_map: bool = False, - chi_squared_map: bool = False, - ): - """ - Plots the individual attributes of the plotter's `FitImagingCI` object in 1D. - - These 1D plots correspond to a region in 2D on the charge injection image, which is binned up over the parallel - or serial direction to produce a 1D plot. For example, for the input `region=parallel_fpr`, this - function extracts the FPR over each charge injection region and bins such that the 1D plot shows the FPR - in the parallel direction. - - The API is such that every plottable attribute of the `FitImagingCI` object is an input parameter of type bool - of the function, which if switched to `True` means that it is plotted. - - Parameters - ---------- - region - The region on the charge injection image where data is extracted and binned over the parallel or serial - direction {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"}. - data - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars and the model-fit overlaid. - data_logy - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars and the y-axis on a log10 scale and the model-fit overlaid. - data - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region. - noise_map - Whether to make a 1D plot (via `plot`) of the noise-map extracted and binned over the region. - signal_to_noise_map - Whether to make a 1D plot (via `plot`) of the signal-to-noise map data extracted and binned over - the region. - pre_cti_data - Whether to make a 1D plot (via `plot`) of the pre-cti data extracted and binned over the region. - post_cti_data - Whether to make a 1D plot (via `plot`) of the post-cti data extracted and binned over the - region. - residual_map - Whether to make a 1D plot (via `plot`) of the residual map extracted and binned over the region, with the - noise-map values included as error bars and the model-fit overlaid. - residual_map_logy - Whether to make a 1D plot (via `plot`) of the residual map extracted and binned over the region, with the - noise-map values included as error bars and the y-axis on a log10 scale and the model-fit overlaid. - normalized_residual_map - Whether to make a 1D plot (via `plot`) of the normalized residual map extracted and binned over the - region. - chi_squared_map - Whether to make a 1D plot (via `plot`) of the chi-squared map extracted and binned over the - region. - """ - - title_str = self.title_str_from(region=region) - - y = self.extract_region_from(array=self.fit.data, region=region) - y_errors = self.extract_region_noise_map_from( - array=self.fit.noise_map, region=region - ) - y_extra = self.extract_region_from(array=self.fit.model_data, region=region) - - should_plot_zero = self.should_plot_zero_from(region=region) - - if data: - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar", - y_errors=y_errors, - y_extra=y_extra, - should_plot_zero=should_plot_zero, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Data {title_str}", - yunit="e-", - filename=f"data_{region}", - ), - ) - - if data_logy: - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar_logy", - y_errors=y_errors, - y_extra=y_extra, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Data {title_str} [log10]", - yunit="e-", - filename=f"data_logy_{region}", - ), - ) - - if noise_map: - y = self.extract_region_from(array=self.fit.data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Noise-Map {title_str}", - yunit="e-", - filename=f"noise_map_{region}", - ), - ) - - if signal_to_noise_map: - y = self.extract_region_from( - array=self.fit.signal_to_noise_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Signal-To-Noise Map {title_str}", - yunit="", - filename=f"signal_to_noise_map_{region}", - ), - ) - - if pre_cti_data: - y = self.extract_region_from(array=self.fit.pre_cti_data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"CI Pre CTI {title_str}", - yunit="e-", - filename=f"pre_cti_data_{region}", - ), - ) - - if post_cti_data: - y = self.extract_region_from(array=self.fit.post_cti_data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - should_plot_zero=should_plot_zero, - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"CI Post CTI {title_str}", - yunit="e-", - filename=f"post_cti_data_{region}", - ), - ) - - if residual_map: - y = self.extract_region_from(array=self.fit.residual_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar", - y_errors=y_errors, - should_plot_zero=should_plot_zero, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Residual Map {title_str}", - yunit="e-", - filename=f"residual_map_{region}", - ), - ) - - if residual_map_logy: - y = self.extract_region_from(array=self.fit.residual_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar_logy", - y_errors=y_errors, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Residual Map {title_str} [log10]", - yunit="e-", - filename=f"residual_map_logy_{region}", - ), - ) - - if normalized_residual_map: - y = self.extract_region_from( - array=self.fit.normalized_residual_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Normalized Residual Map {title_str}", - yunit=r"$\sigma$", - filename=f"normalized_residual_map_{region}", - ), - ) - - if chi_squared_map: - y = self.extract_region_from(array=self.fit.chi_squared_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Chi-Squared Map {region}", - yunit=r"$\chi^2$", - filename=f"chi_squared_map_{region}", - ), - ) - - def figures_1d_data_binned( - self, - rows_fpr: bool = False, - rows_no_fpr: bool = False, - columns_fpr: bool = False, - columns_no_fpr: bool = False, - ): - """ - Plots the charge injection data and fit binned over the parallel and serial directions, with and without the - FPR regions included. - - Plots binned over rows show the FPR of each injection, so that the FPR can be compared between injections. - When the FPR is masked it allows comparison of the parallel EPER of each injection. - - Plots binned over columns shown the charge injection non-uniformity. - - Inaccurate bias subtraction / stray light subtraction and other systematics can produce a gradient over - a full image, which these plots often show. - - Parameters - ---------- - columns_fpr - Whether to plot the data binned over columns with the FPR regions included. - """ - - fpr_mask = self.fpr_mask_from() - - if rows_fpr: - data = copy.copy(self.dataset.data) - y = data.apply_mask(mask=fpr_mask.invert()).binned_across_rows - - model_data = copy.copy(self.fit.model_data) - y_extra = model_data.apply_mask(mask=fpr_mask.invert()).binned_across_rows - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - y_extra=y_extra, - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Data With FPR Binned Over Rows", - yunit="e-", - filename=f"fit_data_binned_rows_fpr", - ), - ) - - if rows_no_fpr: - data = copy.copy(self.dataset.data) - y = data.apply_mask(mask=fpr_mask).binned_across_rows - - model_data = copy.copy(self.fit.model_data) - y_extra = model_data.apply_mask(mask=fpr_mask).binned_across_rows - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - y_extra=y_extra, - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Data No FPR Binned Over Rows", - yunit="e-", - filename=f"fit_data_binned_rows_no_fpr", - ), - ) - - if columns_fpr: - data = copy.copy(self.dataset.data) - y = data.apply_mask(mask=fpr_mask).binned_across_columns - - model_data = copy.copy(self.fit.model_data) - y_extra = model_data.apply_mask(mask=fpr_mask).binned_across_columns - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - y_extra=y_extra, - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Data With FPR Binned Over Columns", - yunit="e-", - filename=f"fit_data_binned_columns_fpr", - ), - ) - - if columns_no_fpr: - data = copy.copy(self.dataset.data) - y = data.apply_mask(mask=fpr_mask).binned_across_columns - - model_data = copy.copy(self.fit.model_data) - y_extra = model_data.apply_mask(mask=fpr_mask).binned_across_columns - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - y_extra=y_extra, - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Data No FPR Binned Over Columns", - yunit="e-", - filename=f"fit_data_binned_columns_no_fpr", - ), - ) - - def subplot( - self, - data: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - post_cti_data: bool = False, - residual_map: bool = False, - normalized_residual_map: bool = False, - chi_squared_map: bool = False, - auto_filename: str = "subplot_fit", - ): - """ - Plots the individual attributes of the plotter's `FitImagingCI` object in 2D on a subplot. - - The API is such that every plottable attribute of the `FitImagingCI` object is an input parameter of type bool - of the function, which if switched to `True` means that it is included on the subplot. - - Parameters - ---------- - data - Whether to include a 2D plot (via `imshow`) of the image data. - noise_map - Whether to include a 2D plot (via `imshow`) noise map. - signal_to_noise_map - Whether to include a 2D plot (via `imshow`) signal-to-noise map. - pre_cti_data - Whether to include a 2D plot (via `imshow`) of the pre-cti data. - post_cti_data - Whether to include a 2D plot (via `imshow`) of the post-cti data. - residual_map - Whether to include a 2D plot (via `imshow`) residual map. - normalized_residual_map - Whether to include a 2D plot (via `imshow`) normalized residual map. - chi_squared_map - Whether to include a 2D plot (via `imshow`) chi-squared map. - auto_filename - The default filename of the output subplot if written to hard-disk. - """ - self._subplot_custom_plot( - data=data, - noise_map=noise_map, - signal_to_noise_map=signal_to_noise_map, - pre_cti_data=pre_cti_data, - post_cti_data=post_cti_data, - residual_map=residual_map, - normalized_residual_map=normalized_residual_map, - chi_squared_map=chi_squared_map, - auto_labels=AutoLabels(filename=auto_filename), - ) - - def subplot_fit(self): - """ - Standard subplot of the attributes of the plotter's `FitImaging` object. - """ - return self.subplot( - data=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - normalized_residual_map=True, - chi_squared_map=True, - ) - - def subplot_1d(self, region: str): - """ - Plots the individual attributes of the plotter's `FitImagingCI` object in 1D on a subplot. - - These 1D plots correspond to a region in 2D on the charge injection image, which is binned up over the parallel - or serial direction to produce a 1D plot. For example, for the input `region=parallel_fpr`, this - function extracts the FPR over each charge injection region and bins such that the 1D plot shows the FPR - in the parallel direction. - - The function plots the image, noise map, pre-cti data and signal to noise map in 1D on the subplot. - - Parameters - ---------- - region - The region on the charge injection image where data is extracted and binned over the parallel or serial - direction {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"} - """ - - self.open_subplot_figure(number_subplots=6) - - self.figures_1d(data=True, region=region) - self.figures_1d(signal_to_noise_map=True, region=region) - self.figures_1d(pre_cti_data=True, region=region) - self.figures_1d(post_cti_data=True, region=region) - self.figures_1d(normalized_residual_map=True, region=region) - self.figures_1d(chi_squared_map=True, region=region) - - self.mat_plot_1d.output.subplot_to_figure( - auto_filename=f"subplot_1d_fit_ci_{region}" - ) - self.close_subplot_figure() - - def subplot_noise_scaling_map_dict(self): - """ - Plots the noise-scaling maps of the plotter's `FitImagingCI` object in 2D on a subplot. - """ - - self.open_subplot_figure(number_subplots=len(self.fit.noise_scaling_map_dict)) - - for key, noise_scaling_map in self.fit.noise_scaling_map_dict.items(): - self.mat_plot_2d.plot_array( - array=noise_scaling_map, - visuals_2d=self.get_visuals_2d(), - auto_labels=AutoLabels(title=f"Noise Scaling Map {key}"), - ) - - self.mat_plot_2d.output.subplot_to_figure( - auto_filename=f"subplot_noise_scaling_map_dict" - ) - self.mat_plot_2d.figure.close() diff --git a/autocti/charge_injection/plot/imaging_ci_plots.py b/autocti/charge_injection/plot/imaging_ci_plots.py new file mode 100644 index 00000000..a6be1566 --- /dev/null +++ b/autocti/charge_injection/plot/imaging_ci_plots.py @@ -0,0 +1,354 @@ +import copy +from typing import List, Optional + +from autoconf import conf + +from autoarray.plot.array import plot_array +from autoarray.plot.utils import ( + subplots, + subplot_save, + conf_subplot_figsize, + tight_layout, +) + +from autocti.charge_injection.imaging.imaging import ImagingCI +from autocti.util import plot_utils + + +def _extract(dataset: ImagingCI, array, region: str): + """Extract and bin a 1D quantity of the charge injection dataset via its layout.""" + return dataset.layout.extract_region_from(array=array, region=region) + + +def _extract_noise(dataset: ImagingCI, array, region: str): + return dataset.layout.extract_region_noise_map_from(array=array, region=region) + + +def figure_data_region( + dataset: ImagingCI, + region: str, + logy: bool = False, + ax=None, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Plot the data of an ``ImagingCI`` dataset extracted and binned over a region (e.g. the parallel FPR) + as a 1D errorbar figure, with the binned noise map as error bars. + + Parameters + ---------- + dataset + The charge injection dataset whose data is plotted. + region + The region extracted and binned {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper", + "fpr_non_uniformity"}. + logy + Whether the y-axis is log10 scaled. + ax + Existing matplotlib axes to draw onto (subplot panel); ``None`` creates a standalone figure. + """ + title_str = plot_utils.title_str_from(dataset=dataset, region=region) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.data, region), + y_errors=_extract_noise(dataset, dataset.noise_map, region), + errorbar=True, + ls_errorbar="-" if region == "fpr_non_uniformity" else "", + logy=logy, + should_plot_zero=plot_utils.should_plot_zero_from(region=region), + text_manual_dict=plot_utils.text_manual_dict_from( + dataset=dataset, region=region + ), + text_manual_dict_y=plot_utils.text_manual_dict_y_from(region=region), + title=_pf(f"Data {title_str}" + (" [log10 y]" if logy else "")), + ax=ax, + output_path=output_path, + output_filename=f"data_logy_{region}" if logy else f"data_{region}", + output_format=output_format, + ) + + +def subplot_dataset( + dataset: ImagingCI, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Subplot of the 2D attributes of an ``ImagingCI`` dataset: the data, noise map, signal-to-noise map, + pre-CTI data and (when present) the cosmic ray map. + + Parameters + ---------- + dataset + The charge injection dataset to visualize. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + title_str = plot_utils.title_str_2d_from(dataset=dataset) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + panels = [ + (dataset.data, "Data"), + (dataset.noise_map, "Noise Map"), + (dataset.signal_to_noise_map, "Signal To Noise Map"), + (dataset.pre_cti_data, "Pre CTI Data"), + ] + + if dataset.cosmic_ray_map is not None: + panels.append((dataset.cosmic_ray_map, "Cosmic Ray Map")) + + n = len(panels) + cols = 3 + rows = (n + cols - 1) // cols + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.flatten() + + for i, (array, title) in enumerate(panels): + plot_array(array, ax=axes[i], title=_pf(title_str or title)) + + for ax in axes[n:]: + ax.axis("off") + + tight_layout() + subplot_save(fig, output_path, "subplot_dataset", output_format) + + +def subplot_dataset_region( + dataset: ImagingCI, + region: str, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Four-panel 1D subplot of an ``ImagingCI`` dataset extracted and binned over a region (e.g. the + parallel FPR): the data (with noise error bars), noise map, pre-CTI data and signal-to-noise map. + + Parameters + ---------- + dataset + The charge injection dataset to visualize. + region + The region extracted and binned {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"}. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + title_str = plot_utils.title_str_from(dataset=dataset, region=region) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + fig, axes = subplots(2, 2, figsize=conf_subplot_figsize(2, 2)) + axes = axes.flatten() + + figure_data_region( + dataset=dataset, region=region, ax=axes[0], title_prefix=title_prefix + ) + + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.noise_map, region), + title=_pf(f"Noise Map {title_str}"), + ylabel="Noise (e-)", + ax=axes[1], + ) + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.pre_cti_data, region), + title=_pf(f"Pre CTI Data {title_str}"), + ax=axes[2], + ) + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.signal_to_noise_map, region), + title=_pf(f"Signal To Noise Map {title_str}"), + ylabel="Signal To Noise", + ax=axes[3], + ) + + tight_layout() + subplot_save(fig, output_path, f"subplot_1d_ci_{region}", output_format) + + +def subplot_data_binned( + dataset: ImagingCI, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Four-panel subplot of the charge injection data binned over the parallel and serial directions, with + and without the FPR regions included. + + Panels binned over rows show the FPR of each injection; with the FPR masked they show the parallel + EPER of each injection. Panels binned over columns show the charge injection non-uniformity, where + inaccurate bias / stray-light subtraction produces visible gradients. + + Parameters + ---------- + dataset + The charge injection dataset to visualize. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + fpr_mask = plot_utils.fpr_mask_from(dataset=dataset) + + panels = [ + (fpr_mask.invert(), "binned_across_rows", "Data With FPR Binned Over Rows"), + (fpr_mask, "binned_across_rows", "Data No FPR Binned Over Rows"), + ( + fpr_mask.invert(), + "binned_across_columns", + "Data With FPR Binned Over Columns", + ), + (fpr_mask, "binned_across_columns", "Data No FPR Binned Over Columns"), + ] + + fig, axes = subplots(2, 2, figsize=conf_subplot_figsize(2, 2)) + axes = axes.flatten() + + text_manual_dict = plot_utils.text_manual_dict_from(dataset=dataset) + text_manual_dict_y = plot_utils.text_manual_dict_y_from() + + for i, (mask, binned_attr, title) in enumerate(panels): + data = copy.copy(dataset.data) + y = getattr(data.apply_mask(mask=mask), binned_attr) + + plot_utils.plot_cti_1d( + y=y, + title=_pf(title), + text_manual_dict=text_manual_dict, + text_manual_dict_y=text_manual_dict_y, + ax=axes[i], + ) + + tight_layout() + subplot_save(fig, output_path, "subplot_data_binned", output_format) + + +def figure_pre_cti_data_residual_map( + dataset: ImagingCI, + ax=None, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Plot the 2D pre-CTI data residual map of an ``ImagingCI`` dataset with a symmetric color map, whose + limits are set via the ``symmetric_cmap_value`` visualize config entry. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + symmetric_value = conf.instance["visualize"]["general"]["general"][ + "symmetric_cmap_value" + ] + + plot_array( + dataset.pre_cti_data_residual_map, + ax=ax, + title=_pf( + plot_utils.title_str_2d_from(dataset=dataset) or "Pre CTI Data Residual Map" + ), + vmin=-symmetric_value, + vmax=symmetric_value, + output_path=output_path, + output_filename="pre_cti_data_residual_map", + output_format=output_format, + ) + + +def subplot_dataset_list( + dataset_list: List[ImagingCI], + output_path=None, + output_filename: str = "subplot_dataset_list", + output_format=None, + title_prefix: str = None, +): + """ + Combined subplot of a list of ``ImagingCI`` datasets (e.g. from a combined analysis), one row per + dataset showing its data, noise map and pre-CTI data. + + Parameters + ---------- + dataset_list + The list of charge injection datasets to visualize. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + rows = len(dataset_list) + cols = 3 + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.reshape(rows, cols) if hasattr(axes, "reshape") else [[axes]] + + for i, dataset in enumerate(dataset_list): + title_str = plot_utils.title_str_2d_from(dataset=dataset) + plot_array(dataset.data, ax=axes[i][0], title=_pf(title_str or f"Data {i}")) + plot_array(dataset.noise_map, ax=axes[i][1], title=_pf(f"Noise Map {i}")) + plot_array(dataset.pre_cti_data, ax=axes[i][2], title=_pf(f"Pre CTI Data {i}")) + + tight_layout() + subplot_save(fig, output_path, output_filename, output_format) + + +def subplot_data_region_list( + dataset_list: List[ImagingCI], + region: str, + logy: bool = False, + output_path=None, + output_filename: str = None, + output_format=None, + title_prefix: str = None, +): + """ + Combined subplot of the binned 1D data of a list of ``ImagingCI`` datasets over a region (e.g. the + parallel FPR), one panel per dataset. + + Parameters + ---------- + dataset_list + The list of charge injection datasets to visualize. + region + The region extracted and binned {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"}. + logy + Whether the y-axes are log10 scaled. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + n = len(dataset_list) + cols = min(n, 3) + rows = (n + cols - 1) // cols + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.flatten() if hasattr(axes, "flatten") else [axes] + + for i, dataset in enumerate(dataset_list): + figure_data_region( + dataset=dataset, + region=region, + logy=logy, + ax=axes[i], + title_prefix=title_prefix, + ) + + for ax in axes[n:]: + ax.axis("off") + + tight_layout() + + if output_filename is None: + output_filename = f"subplot_data{'_logy' if logy else ''}_list_{region}" + + subplot_save(fig, output_path, output_filename, output_format) diff --git a/autocti/charge_injection/plot/imaging_ci_plotters.py b/autocti/charge_injection/plot/imaging_ci_plotters.py deleted file mode 100644 index 110f1905..00000000 --- a/autocti/charge_injection/plot/imaging_ci_plotters.py +++ /dev/null @@ -1,493 +0,0 @@ -import copy - -import numpy as np -from typing import Callable - -from autoconf import conf - -import autoarray.plot as aplt - -from autoarray.plot.auto_labels import AutoLabels -from autoarray.dataset.plot.imaging_plotters import ImagingPlotterMeta - -from autocti.plot.abstract_plotters import Plotter -from autocti.charge_injection.imaging.imaging import ImagingCI - - -class ImagingCIPlotter(Plotter): - def __init__( - self, - dataset: ImagingCI, - mat_plot_2d: aplt.MatPlot2D = aplt.MatPlot2D(), - visuals_2d: aplt.Visuals2D = aplt.Visuals2D(), - include_2d: aplt.Include2D = aplt.Include2D(), - mat_plot_1d: aplt.MatPlot1D = aplt.MatPlot1D(), - visuals_1d: aplt.Visuals1D = aplt.Visuals1D(), - include_1d: aplt.Include1D = aplt.Include1D(), - residuals_symmetric_cmap: bool = True, - ): - """ - Plots the attributes of `Imaging` objects using the matplotlib method `imshow()` and many other matplotlib - functions which customize the plot's appearance. - - The `mat_plot_2d` attribute wraps matplotlib function calls to make the figure. By default, the settings - passed to every matplotlib function called are those specified in the `config/visualize/mat_wrap/*.ini` files, - but a user can manually input values into `MatPlot2d` to customize the figure's appearance. - - Overlaid on the figure are visuals, contained in the `Visuals2D` object. Attributes may be extracted from - the `Imaging` and plotted via the visuals object, if the corresponding entry is `True` in the `Include2D` - object or the `config/visualize/include.ini` file. - - Parameters - ---------- - dataset - The charge injection imaging dataset the plotter plots. - mat_plot_2d - Contains objects which wrap the matplotlib function calls that make 2D plots. - visuals_2d - Contains 2D visuals that can be overlaid on 2D plots. - include_2d - Specifies which attributes of the `Imaging` are extracted and plotted as visuals for 2D plots. - mat_plot_1d - Contains objects which wrap the matplotlib function calls that make 1D plots. - visuals_1d - Contains 1D visuals that can be overlaid on 1D plots. - include_1d - Specifies which attributes of the `ImagingCI` are extracted and plotted as visuals for 1D plots. - residuals_symmetric_cmap - If true, the `pre_cti_residual_map` is plotted with a symmetric color map such that `abs(vmin) = abs(vmax)`. - """ - super().__init__( - dataset=dataset, - mat_plot_2d=mat_plot_2d, - include_2d=include_2d, - visuals_2d=visuals_2d, - ) - - self.visuals_1d = visuals_1d - self.include_1d = include_1d - self.mat_plot_1d = mat_plot_1d - - self._imaging_meta_plotter = ImagingPlotterMeta( - dataset=self.dataset, - get_visuals_2d=self.get_visuals_2d, - mat_plot_2d=self.mat_plot_2d, - include_2d=self.include_2d, - visuals_2d=self.visuals_2d, - ) - - self.residuals_symmetric_cmap = residuals_symmetric_cmap - - def get_visuals_1d(self): - return self.visuals_1d - - def get_visuals_2d(self): - return self.get_2d.via_mask_from(mask=self.dataset.mask) - - @property - def extract_region_from(self) -> Callable: - return self.dataset.layout.extract_region_from - - @property - def extract_region_noise_map_from(self) -> Callable: - return self.dataset.layout.extract_region_noise_map_from - - def figures_2d( - self, - data: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - pre_cti_data_residual_map: bool = False, - cosmic_ray_map: bool = False, - ): - """ - Plots the individual attributes of the plotter's `ImagingCI` object in 2D. - - The API is such that every plottable attribute of the `Imaging` object is an input parameter of type bool of - the function, which if switched to `True` means that it is plotted. - - Parameters - ---------- - data - Whether to make a 2D plot (via `imshow`) of the image data. - noise_map - Whether to make a 2D plot (via `imshow`) of the noise map. - signal_to_noise_map - Whether to make a 2D plot (via `imshow`) of the signal-to-noise map. - pre_cti_data - Whether to make a 2D plot (via `imshow`) of the pre-cti data. - pre_cti_data_residual_map - Whether to make a 2D plot (via `imshow`) of the pre-cti data residual-map. - cosmic_ray_map - Whether to make a 2D plot (via `imshow`) of the cosmic ray map. - """ - - title_str = self.title_str_2d_from() - - self._imaging_meta_plotter.figures_2d( - data=data, - noise_map=noise_map, - signal_to_noise_map=signal_to_noise_map, - title_str=title_str, - ) - - if pre_cti_data: - self.mat_plot_2d.plot_array( - array=self.dataset.pre_cti_data, - visuals_2d=self.get_visuals_2d(), - auto_labels=AutoLabels( - title=title_str or f"Pre CTI Data", filename="pre_cti_data" - ), - ) - - cmap_original = self.mat_plot_2d.cmap - - if self.residuals_symmetric_cmap: - symmetric_value = conf.instance["visualize"]["general"]["general"][ - "symmetric_cmap_value" - ] - - self.mat_plot_2d.cmap = self.mat_plot_2d.cmap.symmetric_cmap_from( - symmetric_value=symmetric_value - ) - - if pre_cti_data_residual_map: - self.mat_plot_2d.plot_array( - array=self.dataset.pre_cti_data_residual_map, - visuals_2d=self.get_visuals_2d(), - auto_labels=AutoLabels( - title=title_str or f"Pre CTI Data Residual Map", - filename="pre_cti_data_residual_map", - ), - ) - - self.mat_plot_2d.cmap = cmap_original - - if cosmic_ray_map: - self.mat_plot_2d.plot_array( - array=self.dataset.cosmic_ray_map, - visuals_2d=self.get_visuals_2d(), - auto_labels=AutoLabels( - title=title_str or f"Cosmic Ray Map", filename="cosmic_ray_map" - ), - ) - - def figures_1d( - self, - region: str, - data: bool = False, - data_logy: bool = False, - noise_map: bool = False, - pre_cti_data: bool = False, - signal_to_noise_map: bool = False, - ): - """ - Plots the individual attributes of the plotter's `ImagingCI` object in 1D. - - The API is such that every plottable attribute of the `Imaging` object is an input parameter of type bool of - the function, which if switched to `True` means that it is plotted. - - if a `region` string is input, the 1D plots correspond to a region in 2D on the charge injection image, which - is binned up over the parallel or serial direction to produce a 1D plot. For example, for the - input `region=parallel_fpr`, this function extracts the FPR over each charge injection region and bins such - that the 1D plot shows the FPR in the parallel direction. - - The API is such that every plottable attribute of the `Imaging` object is an input parameter of type bool of - the function, which if switched to `True` means that it is plotted. - - Parameters - ---------- - region - The region on the charge injection image where data is extracted and binned over the parallel or serial - direction {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"} - data - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars. - data_logy - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars and the y-axis on a log10 scale. - noise_map - Whether to make a 1D plot (via `plot`) of the noise-map extracted and binned over the region. - pre_cti_data - Whether to make a 1D plot (via `plot`) of the pre-cti data extracted and binned over the region. - signal_to_noise_map - Whether to make a 1D plot (via `plot`) of the signal-to-noise map data extracted and binned over - the region. - """ - - if region == "fpr_non_uniformity": - ls_errorbar = "-" - else: - ls_errorbar = "" - - should_plot_zero = self.should_plot_zero_from(region=region) - - title_str = self.title_str_from(region=region) - - if data: - y = self.extract_region_from(array=self.dataset.data, region=region) - y_errors = self.extract_region_noise_map_from( - array=self.dataset.noise_map, region=region - ) - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar", - y_errors=y_errors, - ls_errorbar=ls_errorbar, - should_plot_zero=should_plot_zero, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data {title_str}", - yunit="e-", - filename=f"data_{region}", - ), - ) - - if data_logy: - y = self.extract_region_from(array=self.dataset.data, region=region) - y_errors = self.extract_region_noise_map_from( - array=self.dataset.noise_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar_logy", - y_errors=y_errors, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data {title_str} [log10 y]", - yunit="e-", - filename=f"data_logy_{region}", - ), - ) - - if noise_map: - y = self.extract_region_from(array=self.dataset.noise_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Noise Map {title_str}", - ylabel="Noise (e-)", - filename=f"noise_map_{region}", - ), - ) - - if pre_cti_data: - y = self.extract_region_from(array=self.dataset.pre_cti_data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"CI Pre CTI {title_str}", - yunit="e-", - filename=f"pre_cti_data_{region}", - ), - ) - - if signal_to_noise_map: - y = self.extract_region_from( - array=self.dataset.signal_to_noise_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Signal To Noise Map {title_str}", - ylabel="Signal To Noise (e-)", - filename=f"signal_to_noise_map_{region}", - ), - ) - - def figures_1d_data_binned( - self, - rows_fpr: bool = False, - rows_no_fpr: bool = False, - columns_fpr: bool = False, - columns_no_fpr: bool = False, - ): - """ - Plots the charge injection data binned over the parallel and serial directions, with and without the - FPR regions included. - - Plots binned over rows show the FPR of each injection, so that the FPR can be compared between injections. - When the FPR is masked it allows comparison of the parallel EPER of each injection. - - Plots binned over columns shown the charge injection non-uniformity. - - Inaccurate bias subtraction / stray light subtraction and other systematics can produce a gradient over - a full image, which these plots often show. - - Parameters - ---------- - columns_fpr - Whether to plot the data binned over columns with the FPR regions included. - """ - - fpr_mask = self.fpr_mask_from() - - if rows_fpr: - data = copy.copy(self.dataset.data) - - y = data.apply_mask(mask=fpr_mask.invert()).binned_across_rows - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data With FPR Binned Over Rows", - yunit="e-", - filename=f"data_binned_rows_fpr", - ), - ) - - if rows_no_fpr: - data = copy.copy(self.dataset.data) - - y = data.apply_mask(mask=fpr_mask).binned_across_rows - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data No FPR Binned Over Rows", - yunit="e-", - filename=f"data_binned_rows_no_fpr", - ), - ) - - if columns_fpr: - data = copy.copy(self.dataset.data) - - y = data.apply_mask(mask=fpr_mask).binned_across_columns - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data With FPR Binned Over Columns", - yunit="e-", - filename=f"data_binned_columns_fpr", - ), - ) - - if columns_no_fpr: - data = copy.copy(self.dataset.data) - - y = data.apply_mask(mask=fpr_mask).binned_across_columns - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - text_manual_dict=self.text_manual_dict_from(), - text_manual_dict_y=self.text_manual_dict_y_from(), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data No FPR Binned Over Columns", - yunit="e-", - filename=f"data_binned_columns_no_fpr", - ), - ) - - def subplot( - self, - data: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - cosmic_ray_map: bool = False, - auto_filename="subplot_dataset", - ): - """ - Plots the individual attributes of the plotter's `ImagingCI` object in 2D on a subplot. - - The API is such that every plottable attribute of the `Imaging` object is an input parameter of type bool of - the function, which if switched to `True` means that it is included on the subplot. - - Parameters - ---------- - data - Whether to include a 2D plot (via `imshow`) of the image data. - noise_map - Whether to include a 2D plot (via `imshow`) of the noise map. - signal_to_noise_map - Whether to include a 2D plot (via `imshow`) of the signal-to-noise map. - pre_cti_data - Whether to include a 2D plot (via `imshow`) of the pre-cti data. - cosmic_ray_map - Whether to include a 2D plot (via `imshow`) of the cosmic ray map. - auto_filename - The default filename of the output subplot if written to hard-disk. - """ - self._subplot_custom_plot( - data=data, - noise_map=noise_map, - signal_to_noise_map=signal_to_noise_map, - pre_cti_data=pre_cti_data, - cosmic_ray_map=cosmic_ray_map, - auto_labels=AutoLabels(filename=auto_filename), - ) - - def subplot_dataset(self): - """ - Standard subplot of the attributes of the plotter's `ImagingCI` object. - """ - self.subplot( - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - cosmic_ray_map=True, - ) - - def subplot_1d(self, region: str): - """ - Plots the individual attributes of the plotter's `ImagingCI` object in 1D on a subplot. - - These 1D plots correspond to a region in 2D on the charge injection image, which is binned up over the parallel - or serial direction to produce a 1D plot. For example, for the input `region=parallel_fpr`, this - function extracts the FPR over each charge injection region and bins such that the 1D plot shows the FPR - in the parallel direction. - - The function plots the image, noise map, pre-cti data and signal to noise map in 1D on the subplot. - - Parameters - ---------- - region - The region on the charge injection image where data is extracted and binned over the parallel or serial - direction {"parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"} - """ - - self.open_subplot_figure(number_subplots=4) - - self.figures_1d(data=True, region=region) - self.figures_1d(noise_map=True, region=region) - self.figures_1d(pre_cti_data=True, region=region) - self.figures_1d(signal_to_noise_map=True, region=region) - - self.mat_plot_1d.output.subplot_to_figure( - auto_filename=f"subplot_1d_ci_{region}" - ) - self.close_subplot_figure() diff --git a/autocti/config/visualize.yaml b/autocti/config/visualize.yaml index 7f5aea9e..cc0a345c 100644 --- a/autocti/config/visualize.yaml +++ b/autocti/config/visualize.yaml @@ -6,25 +6,20 @@ general: symmetric_cmap_value: 100.0 # The vmin and vmax of all pre-cti data residual-maps. subplot_ascending_fpr: true # If True, subplots showing FPR / EPER trails of many datasets are in ascending order of FPR value. plots: - combined_only: false + subplot_format: [png] # Output format of all plots, can be png, pdf or both (e.g. [png, pdf]). + combined_only: false # If True, only the combined subplots of multi-dataset analyses are output (no per-dataset visualization). dataset: - data: true - data_logy: true - cosmic_ray_map: false - pre_cti_data: false - fpr_non_uniformity: false - rows_fpr: true # Plot the data, binned over rows, where only the FPR is visible? - rows_no_fpr: true # Plot the data, binned over rows, where the FPR is masked? - columns_fpr: true # Plot the data, binned over columns, where only the FPR is visible? - columns_no_fpr: true # Plot the data, binned over columns, where the FPR is masked? + subplot_dataset: true # Plot the subplot of all dataset quantities (2D for charge injection imaging, 1D for Dataset1D)? + subplot_dataset_regions: true # Plot per-region binned 1D subplots (e.g. the parallel/serial FPR and EPER)? + data: true # Plot single 1D figures of the data extracted and binned over each region? + data_logy: true # Plot single 1D figures of the data over each region with a log10 y-axis? + data_binned: true # Plot the data binned over rows / columns with and without the FPR (charge injection only)? + fpr_non_uniformity: false # Include the fpr_non_uniformity region in the per-region plots (charge injection only)? fit: - data: true - data_logy: true - residual_map: true - residual_map_logy: true - post_cti_data: false - pre_cti_data: false - rows_fpr: true # Plot the data, binned over rows, where only the FPR is visible? - rows_no_fpr: true # Plot the data, binned over rows, where the FPR is masked? - columns_fpr: true # Plot the data, binned over columns, where only the FPR is visible? - columns_no_fpr: true # Plot the data, binned over columns, where the FPR is masked? + subplot_fit: true # Plot the subplot of all fit quantities (e.g. model data, residual-map, chi-squared map)? + subplot_fit_regions: true # Plot per-region binned 1D fit subplots (e.g. the parallel/serial FPR and EPER)? + data: true # Plot single 1D figures of the fit data (with model overlay) over each region? + data_logy: true # Plot single 1D figures of the fit data over each region with a log10 y-axis? + residual_map: true # Plot single 1D figures of the residual map over each region? + residual_map_logy: true # Plot single 1D figures of the residual map over each region with a log10 y-axis? + fits_fit: true # Output a fit.fits file containing the model data, residual map, normalized residual map and chi-squared map? diff --git a/autocti/dataset_1d/model/plotter.py b/autocti/dataset_1d/model/plotter.py new file mode 100644 index 00000000..77d16891 --- /dev/null +++ b/autocti/dataset_1d/model/plotter.py @@ -0,0 +1,327 @@ +import logging +from typing import List + +from autocti.dataset_1d.dataset_1d.dataset_1d import Dataset1D +from autocti.dataset_1d.fit import FitDataset1D +from autocti.dataset_1d.plot import dataset_1d_plots +from autocti.dataset_1d.plot import fit_plots +from autocti.model.plotter import Plotter, plot_setting + +from autocti import exc + +logger = logging.getLogger(__name__) + + +class PlotterDataset1D(Plotter): + """ + Outputs visualization of ``Dataset1D`` datasets and fits during a model fit, using the matplotlib + function API in ``autocti.dataset_1d.plot``. + + The images output are customized via the ``plots`` section of ``config/visualize.yaml``. + """ + + def dataset(self, dataset: Dataset1D, folder_suffix: str = ""): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset{folder_suffix}" + + if should_plot("subplot_dataset"): + dataset_1d_plots.subplot_dataset( + dataset=dataset, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data"): + dataset_1d_plots.figure_data( + dataset=dataset, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + dataset_1d_plots.figure_data( + dataset=dataset, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + def dataset_regions( + self, dataset: Dataset1D, region_list: List[str], folder_suffix: str = "" + ): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset{folder_suffix}" + + for region in region_list: + try: + if should_plot("subplot_dataset_regions"): + dataset_1d_plots.subplot_dataset( + dataset=dataset, + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data"): + dataset_1d_plots.figure_data( + dataset=dataset, + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + dataset_1d_plots.figure_data( + dataset=dataset, + region=region, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the Dataset1D {region}" + ) + + def dataset_combined(self, dataset_list: List[Dataset1D], folder_suffix: str = ""): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset{folder_suffix}" + + if should_plot("subplot_dataset"): + dataset_1d_plots.subplot_dataset_list( + dataset_list=dataset_list, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + def dataset_regions_combined( + self, + dataset_list: List[Dataset1D], + region_list: List[str], + folder_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="dataset", name=name) + + output_path = self.image_path / f"dataset{folder_suffix}" + + for region in region_list: + try: + if should_plot("data"): + dataset_1d_plots.subplot_dataset_list( + dataset_list=dataset_list, + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + dataset_1d_plots.subplot_dataset_list( + dataset_list=dataset_list, + region=region, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the Dataset1D {region}" + ) + + def fit(self, fit: FitDataset1D, during_analysis: bool, folder_suffix: str = ""): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset{folder_suffix}" + + if should_plot("subplot_fit"): + fit_plots.subplot_fit( + fit=fit, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data"): + fit_plots.figure_fit( + fit=fit, + quantity="data", + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + fit_plots.figure_fit( + fit=fit, + quantity="data", + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("residual_map"): + fit_plots.figure_fit( + fit=fit, + quantity="residual_map", + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("residual_map_logy"): + fit_plots.figure_fit( + fit=fit, + quantity="residual_map", + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if not during_analysis: + if should_plot("fits_fit"): + fit_plots.fits_fit(fit=fit, output_path=output_path) + + def fit_regions( + self, + fit: FitDataset1D, + region_list: List[str], + during_analysis: bool, + folder_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset{folder_suffix}" + + for region in region_list: + try: + if should_plot("subplot_fit_regions"): + fit_plots.subplot_fit( + fit=fit, + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data"): + fit_plots.figure_fit( + fit=fit, + quantity="data", + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + fit_plots.figure_fit( + fit=fit, + quantity="data", + region=region, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the Dataset1D {region}" + ) + + def fit_combined( + self, + fit_list: List[FitDataset1D], + during_analysis: bool, + folder_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset_combined{folder_suffix}" + + for quantity, key in [ + ("data", "data"), + ("residual_map", "residual_map"), + ("normalized_residual_map", "residual_map"), + ("chi_squared_map", "residual_map"), + ]: + if should_plot(key): + fit_plots.subplot_fit_list( + fit_list=fit_list, + quantity=quantity, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + def fit_region_combined( + self, + fit_list: List[FitDataset1D], + region_list: List[str], + during_analysis: bool, + folder_suffix: str = "", + ): + def should_plot(name): + return plot_setting(section="fit", name=name) + + output_path = self.image_path / f"fit_dataset_combined{folder_suffix}" + + for region in region_list: + try: + if should_plot("data"): + fit_plots.subplot_fit_list( + fit_list=fit_list, + quantity="data", + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("data_logy"): + fit_plots.subplot_fit_list( + fit_list=fit_list, + quantity="data", + region=region, + logy=True, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + if should_plot("residual_map"): + fit_plots.subplot_fit_list( + fit_list=fit_list, + quantity="residual_map", + region=region, + output_path=output_path, + output_format=self.fmt, + title_prefix=self.title_prefix, + ) + + except (exc.RegionException, TypeError, ValueError): + logger.info( + f"VISUALIZATION - Could not visualize the Dataset1D {region}" + ) diff --git a/autocti/dataset_1d/model/plotter_interface.py b/autocti/dataset_1d/model/plotter_interface.py deleted file mode 100644 index 7d8f7f3f..00000000 --- a/autocti/dataset_1d/model/plotter_interface.py +++ /dev/null @@ -1,307 +0,0 @@ -import logging - -import autocti.plot as aplt - -from autocti.model.plotter_interface import PlotterInterface -from autocti.model.plotter_interface import plot_setting -from autocti import exc - -logger = logging.getLogger(__name__) - -logger.setLevel(level="INFO") - - -class PlotterInterfaceDataset1D(PlotterInterface): - def dataset(self, dataset, folder_suffix=""): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot_1d = self.mat_plot_1d_from(subfolders=f"dataset{folder_suffix}") - - dataset_plotter = aplt.Dataset1DPlotter( - dataset=dataset, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - - if should_plot("subplot_dataset"): - dataset_plotter.subplot_dataset() - - dataset_plotter.figures_1d( - data=should_plot("data"), - data_logy=should_plot("data_logy"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - ) - - def dataset_regions(self, dataset, region_list, folder_suffix=""): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot_1d = self.mat_plot_1d_from(subfolders=f"dataset{folder_suffix}") - - dataset_plotter = aplt.Dataset1DPlotter( - dataset=dataset, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - - for region in region_list: - try: - if should_plot("subplot_dataset"): - dataset_plotter.subplot_dataset(region=region) - - dataset_plotter.figures_1d( - region=region, - data=should_plot("data"), - data_logy=should_plot("data_logy"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - ) - - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the Dataset1D {region}" - ) - - def dataset_combined(self, dataset_list, folder_suffix=""): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot_1d = self.mat_plot_1d_from(subfolders=f"dataset{folder_suffix}") - - dataset_plotter_list = [ - aplt.Dataset1DPlotter( - dataset=dataset, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - for dataset in dataset_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=dataset_plotter_list) - - if should_plot("subplot_dataset"): - multi_plotter.subplot_of_figure(func_name="figures_1d", figure_name="data") - - def dataset_regions_combined(self, dataset_list, region_list, folder_suffix=""): - def should_plot(name): - return plot_setting(section="dataset", name=name) - - mat_plot_1d = self.mat_plot_1d_from(subfolders=f"dataset{folder_suffix}") - - dataset_plotter_list = [ - aplt.Dataset1DPlotter( - dataset=dataset, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - for dataset in dataset_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=dataset_plotter_list) - - for region in region_list: - try: - if should_plot("data"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("data_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data_logy", - region=region, - filename_suffix=f"_{region}", - ) - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the Dataset1D {region}" - ) - - def fit(self, fit, during_analysis, folder_suffix=""): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot_1d = self.mat_plot_1d_from(subfolders=f"fit_dataset{folder_suffix}") - - fit_plotter = aplt.FitDataset1DPlotter( - fit=fit, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - - fit_plotter.figures_1d( - data=should_plot("data"), - data_logy=should_plot("data_logy"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - post_cti_data=should_plot("post_cti_data"), - residual_map=should_plot("residual_map"), - residual_map_logy=should_plot("residual_map_logy"), - normalized_residual_map=should_plot("normalized_residual_map"), - chi_squared_map=should_plot("chi_squared_map"), - ) - - if not during_analysis: - if should_plot("all_at_end_png"): - fit_plotter.figures_1d( - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - residual_map=True, - residual_map_logy=True, - normalized_residual_map=True, - chi_squared_map=True, - ) - - if should_plot("subplot_fit"): - fit_plotter.subplot_fit() - - def fit_regions(self, fit, region_list, during_analysis, folder_suffix=""): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot_1d = self.mat_plot_1d_from(subfolders=f"fit_dataset{folder_suffix}") - - fit_plotter = aplt.FitDataset1DPlotter( - fit=fit, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - - for region in region_list: - try: - if should_plot("subplot_fit"): - fit_plotter.subplot_fit(region=region) - - fit_plotter.figures_1d( - region=region, - data=should_plot("data"), - data_logy=should_plot("data_logy"), - noise_map=should_plot("noise_map"), - signal_to_noise_map=should_plot("signal_to_noise_map"), - pre_cti_data=should_plot("pre_cti_data"), - post_cti_data=should_plot("post_cti_data"), - residual_map=should_plot("residual_map"), - residual_map_logy=should_plot("residual_map_logy"), - normalized_residual_map=should_plot("normalized_residual_map"), - chi_squared_map=should_plot("chi_squared_map"), - ) - - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the Dataset1D {region}" - ) - - def fit_combined(self, fit_list, during_analysis, folder_suffix=""): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot_1d = self.mat_plot_1d_from( - subfolders=f"fit_dataset_combined{folder_suffix}" - ) - - fit_plotter_list = [ - aplt.FitDataset1DPlotter( - fit=fit, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - for fit in fit_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=fit_plotter_list) - - if should_plot("data"): - multi_plotter.subplot_of_figure(func_name="figures_1d", figure_name="data") - - if should_plot("data_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", figure_name="data_logy" - ) - - if should_plot("residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", figure_name="residual_map" - ) - - if should_plot("residual_map_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", figure_name="residual_map_logy" - ) - - if should_plot("normalized_residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", figure_name="normalized_residual_map" - ) - - if should_plot("chi_squared_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", figure_name="chi_squared_map" - ) - - def fit_region_combined( - self, fit_list, region_list, during_analysis, folder_suffix="" - ): - def should_plot(name): - return plot_setting(section="fit", name=name) - - mat_plot_1d = self.mat_plot_1d_from( - subfolders=f"fit_dataset_combined{folder_suffix}" - ) - - fit_plotter_list = [ - aplt.FitDataset1DPlotter( - fit=fit, mat_plot_1d=mat_plot_1d, include_1d=self.include_1d - ) - for fit in fit_list - ] - multi_plotter = aplt.MultiFigurePlotter(plotter_list=fit_plotter_list) - - for region in region_list: - try: - if should_plot("data"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("data_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="data_logy", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="residual_map", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("residual_map_logy"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="residual_map_logy", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("normalized_residual_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="normalized_residual_map", - region=region, - filename_suffix=f"_{region}", - ) - - if should_plot("chi_squared_map"): - multi_plotter.subplot_of_figure( - func_name="figures_1d", - figure_name="chi_squared_map", - region=region, - filename_suffix=f"_{region}", - ) - - except (exc.RegionException, TypeError, ValueError): - logger.info( - f"VISUALIZATION - Could not visualize the Dataset1D {region}" - ) diff --git a/autocti/dataset_1d/model/visualizer.py b/autocti/dataset_1d/model/visualizer.py index 996d7519..dfce1f87 100644 --- a/autocti/dataset_1d/model/visualizer.py +++ b/autocti/dataset_1d/model/visualizer.py @@ -2,6 +2,8 @@ import autofit as af +from autocti.dataset_1d.model.plotter import PlotterDataset1D + logger = logging.getLogger(__name__) @@ -26,23 +28,9 @@ def visualize_before_fit( The model object, which includes model components representing the galaxies that are fitted to the imaging data. """ - # Imported lazily: the PlotterInterface stack still targets the removed - # autoarray Plotter API and is rewritten on the new matplotlib function - # API in Phase 1 of the CTI resurrection epic (PyAutoCTI#82). - try: - from autocti.dataset_1d.model.plotter_interface import ( - PlotterInterfaceDataset1D, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - region_list = analysis.region_list_from() - visualizer = PlotterInterfaceDataset1D(image_path=paths.image_path) + visualizer = PlotterDataset1D(image_path=paths.image_path) visualizer.dataset(dataset=analysis.dataset) visualizer.dataset_regions(dataset=analysis.dataset, region_list=region_list) @@ -63,18 +51,7 @@ def visualize_before_fit_combined( if analyses is None: return - try: - from autocti.dataset_1d.model.plotter_interface import ( - PlotterInterfaceDataset1D, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - - plotter = PlotterInterfaceDataset1D(image_path=paths.image_path) + plotter = PlotterDataset1D(image_path=paths.image_path) region_list = analyses[0].region_list_from() @@ -137,20 +114,9 @@ def visualize( If True the visualization is being performed midway through the non-linear search before it is finished, which may change which images are output. """ - try: - from autocti.dataset_1d.model.plotter_interface import ( - PlotterInterfaceDataset1D, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - region_list = analysis.region_list_from() - visualizer = PlotterInterfaceDataset1D(image_path=paths.image_path) + visualizer = PlotterDataset1D(image_path=paths.image_path) fit = analysis.fit_via_instance_from(instance=instance) visualizer.fit(fit=fit, during_analysis=during_analysis) @@ -177,17 +143,6 @@ def visualize_combined( if analyses is None: return - try: - from autocti.dataset_1d.model.plotter_interface import ( - PlotterInterfaceDataset1D, - ) - except ImportError: - logger.warning( - "PyAutoCTI visualization is disabled until the Phase 1 " - "Plotter->matplotlib migration (PyAutoCTI#82)." - ) - return - fit_list = [ analysis.fit_via_instance_from(instance=instance) for analysis in analyses ] @@ -201,7 +156,7 @@ def visualize_combined( region_list = analyses[0].region_list_from() - visualizer = PlotterInterfaceDataset1D(image_path=paths.image_path) + visualizer = PlotterDataset1D(image_path=paths.image_path) visualizer.fit_combined(fit_list=fit_list, during_analysis=during_analysis) visualizer.fit_region_combined( fit_list=fit_list, diff --git a/autocti/dataset_1d/plot/dataset_1d_plots.py b/autocti/dataset_1d/plot/dataset_1d_plots.py new file mode 100644 index 00000000..c2e58113 --- /dev/null +++ b/autocti/dataset_1d/plot/dataset_1d_plots.py @@ -0,0 +1,169 @@ +from typing import List, Optional + +from autoarray.plot.utils import ( + subplots, + subplot_save, + conf_subplot_figsize, + tight_layout, +) + +from autocti.dataset_1d.dataset_1d.dataset_1d import Dataset1D +from autocti.util import plot_utils + + +def _extract(dataset: Dataset1D, array, region: Optional[str]): + """Extract (and for a region, bin) a 1D quantity of the dataset via its layout.""" + return dataset.layout.extract_region_from(array=array, region=region) + + +def figure_data( + dataset: Dataset1D, + region: Optional[str] = None, + logy: bool = False, + ax=None, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Plot the data of a ``Dataset1D`` (optionally extracted and binned over a region, e.g. the FPR or EPER) + as a 1D errorbar figure, with the noise map as error bars. + + Parameters + ---------- + dataset + The 1D dataset whose data is plotted. + region + The region extracted and binned {"fpr", "eper"}; ``None`` plots the full dataset. + logy + Whether the y-axis is log10 scaled. + ax + Existing matplotlib axes to draw onto (subplot panel); ``None`` creates a standalone figure. + """ + suffix = f"_{region}" if region is not None else "" + title_str = plot_utils.title_str_from(dataset=dataset, region=region) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.data, region), + y_errors=_extract(dataset, dataset.noise_map, region), + errorbar=True, + logy=logy, + should_plot_zero=plot_utils.should_plot_zero_from(region=region), + text_manual_dict=plot_utils.text_manual_dict_from( + dataset=dataset, region=region + ), + text_manual_dict_y=plot_utils.text_manual_dict_y_from(region=region), + title=_pf(f"Data 1D {title_str}" + (" [log10]" if logy else "")), + ax=ax, + output_path=output_path, + output_filename=f"data_logy{suffix}" if logy else f"data{suffix}", + output_format=output_format, + ) + + +def subplot_dataset( + dataset: Dataset1D, + region: Optional[str] = None, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Four-panel subplot of a ``Dataset1D``: the data (with noise error bars), noise map, signal-to-noise + map and pre-CTI data, each optionally extracted and binned over a region (e.g. the FPR or EPER). + + Parameters + ---------- + dataset + The 1D dataset to visualize. + region + The region extracted and binned {"fpr", "eper"}; ``None`` plots the full dataset. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + suffix = f"_{region}" if region is not None else "" + title_str = plot_utils.title_str_from(dataset=dataset, region=region) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + fig, axes = subplots(2, 2, figsize=conf_subplot_figsize(2, 2)) + axes = axes.flatten() + + figure_data(dataset=dataset, region=region, ax=axes[0], title_prefix=title_prefix) + + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.noise_map, region), + title=_pf(f"Noise Map {title_str}"), + ax=axes[1], + ) + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.signal_to_noise_map, region), + title=_pf(f"Signal To Noise Map {title_str}"), + ylabel="", + ax=axes[2], + ) + plot_utils.plot_cti_1d( + y=_extract(dataset, dataset.pre_cti_data, region), + title=_pf(f"Pre CTI Data {title_str}"), + ax=axes[3], + ) + + tight_layout() + subplot_save(fig, output_path, f"subplot_dataset{suffix}", output_format) + + +def subplot_dataset_list( + dataset_list: List[Dataset1D], + region: Optional[str] = None, + logy: bool = False, + output_path=None, + output_filename: str = None, + output_format=None, + title_prefix: str = None, +): + """ + Combined subplot of the data of a list of ``Dataset1D`` objects (e.g. from a combined analysis of + multiple datasets), one panel per dataset. + + Parameters + ---------- + dataset_list + The list of 1D datasets to visualize. + region + The region extracted and binned {"fpr", "eper"}; ``None`` plots the full datasets. + logy + Whether the y-axes are log10 scaled. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + suffix = f"_{region}" if region is not None else "" + + n = len(dataset_list) + cols = min(n, 3) + rows = (n + cols - 1) // cols + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.flatten() if hasattr(axes, "flatten") else [axes] + + for i, dataset in enumerate(dataset_list): + figure_data( + dataset=dataset, + region=region, + logy=logy, + ax=axes[i], + title_prefix=title_prefix, + ) + + for ax in axes[n:]: + ax.axis("off") + + tight_layout() + + if output_filename is None: + output_filename = ( + f"subplot_data_logy_list{suffix}" if logy else f"subplot_data_list{suffix}" + ) + + subplot_save(fig, output_path, output_filename, output_format) diff --git a/autocti/dataset_1d/plot/dataset_1d_plotters.py b/autocti/dataset_1d/plot/dataset_1d_plotters.py deleted file mode 100644 index 09021e54..00000000 --- a/autocti/dataset_1d/plot/dataset_1d_plotters.py +++ /dev/null @@ -1,246 +0,0 @@ -from typing import Callable, Optional - -import autoarray.plot as aplt - -from autoarray.plot.auto_labels import AutoLabels - -from autocti.plot.abstract_plotters import Plotter -from autocti.dataset_1d.dataset_1d.dataset_1d import Dataset1D - - -class Dataset1DPlotter(Plotter): - def __init__( - self, - dataset: Dataset1D, - mat_plot_1d: aplt.MatPlot1D = aplt.MatPlot1D(), - visuals_1d: aplt.Visuals1D = aplt.Visuals1D(), - include_1d: aplt.Include1D = aplt.Include1D(), - ): - """ - Plots the attributes of `Dataset1D` objects using the matplotlib method `line()` and many other matplotlib - functions which customize the plot's appearance. - - The `mat_plot_1d` attribute wraps matplotlib function calls to make the figure. By default, the settings - passed to every matplotlib function called are those specified in the `config/visualize/mat_wrap/*.ini` files, - but a user can manually input values into `MatPlot1d` to customize the figure's appearance. - - Overlaid on the figure are visuals, contained in the `Visuals1D` object. Attributes may be extracted from - the `Imaging` and plotted via the visuals object, if the corresponding entry is `True` in the `Include1D` - object or the `config/visualize/include.ini` file. - - Parameters - ---------- - dataset - The dataset 1d the plotter plots. - mat_plot_1d - Contains objects which wrap the matplotlib function calls that make 1D plots. - visuals_1d - Contains 1D visuals that can be overlaid on 1D plots. - include_1d - Specifies which attributes of the `ImagingCI` are extracted and plotted as visuals for 1D plots. - """ - super().__init__( - dataset=dataset, - mat_plot_1d=mat_plot_1d, - include_1d=include_1d, - visuals_1d=visuals_1d, - ) - - def get_visuals_1d(self) -> aplt.Visuals1D: - return self.visuals_1d - - @property - def extract_region_from(self) -> Callable: - return self.dataset.layout.extract_region_from - - def figures_1d( - self, - region: Optional[str] = None, - data: bool = False, - data_logy: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - ): - """ - Plots the individual attributes of the plotter's `Dataset1D` object in 1D. - - The API is such that every plottable attribute of the `Imaging` object is an input parameter of type bool of - the function, which if switched to `True` means that it is plotted. - - If a `region` string is input, the 1D plots correspond to regions in 1D on the 1D dataset, which are binned up - to produce a1D plot. - - For example, for the input `region=fpr`, this function extracts the FPR over each charge region and bins them - such that the 1D plot shows the average FPR. - - The API is such that every plottable attribute of the `Dataset1D` object is an input parameter of type bool of - the function, which if switched to `True` means that it is plotted. - - Parameters - ---------- - region - The region on the 1D dataset where data is extracted and binned {fpr", "eper"} - data - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars. - data_logy - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars and the y-axis on a log10 scale. - noise_map - Whether to make a 1D plot (via `plot`) of the noise-map extracted and binned over the region. - pre_cti_data - Whether to make a 1D plot (via `plot`) of the pre-cti data extracted and binned over the region. - signal_to_noise_map - Whether to make a 1D plot (via `plot`) of the signal-to-noise map data extracted and binned over - the region. - """ - - suffix = f"_{region}" if region is not None else "" - title_str = self.title_str_from(region=region) - - should_plot_zero = self.should_plot_zero_from(region=region) - - if data: - y = self.extract_region_from(array=self.dataset.data, region=region) - y_errors = self.extract_region_from( - array=self.dataset.noise_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar", - y_errors=y_errors, - visuals_1d=self.get_visuals_1d(), - should_plot_zero=should_plot_zero, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - auto_labels=AutoLabels( - title=f"Data 1D {title_str}", - yunit="e-", - filename=f"data{suffix}", - ), - ) - - if data_logy: - y = self.extract_region_from(array=self.dataset.data, region=region) - y_errors = self.extract_region_from( - array=self.dataset.noise_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar_logy", - y_errors=y_errors, - visuals_1d=self.get_visuals_1d(), - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - auto_labels=AutoLabels( - title=f"Data 1D {title_str} [log10]", - yunit="e-", - filename=f"data_logy{suffix}", - ), - ) - - if noise_map: - y = self.extract_region_from(array=self.dataset.noise_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Noise Map {title_str}", - yunit="e-", - filename=f"noise_map{suffix}", - ), - ) - - if pre_cti_data: - y = self.extract_region_from(array=self.dataset.pre_cti_data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"CI Pre CTI {title_str}", - yunit="e-", - filename=f"pre_cti_data{suffix}", - ), - ) - - if signal_to_noise_map: - y = self.extract_region_from( - array=self.dataset.signal_to_noise_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.visuals_1d, - auto_labels=AutoLabels( - title=f"Signal To Noise Map {title_str}", - yunit="", - filename=f"signal_to_noise_map{suffix}", - ), - ) - - def subplot( - self, - data: bool = False, - data_logy: bool = False, - noise_map=False, - signal_to_noise_map=False, - pre_cti_data=False, - auto_filename="subplot_dataset", - **kwargs, - ): - """ - Plots the individual attributes of the plotter's `Dataset1D` object in 1D on a subplot. - - The API is such that every plottable attribute of the `Imaging` object is an input parameter of type bool of - the function, which if switched to `True` means that it is included on the subplot. - - Parameters - ---------- - data - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars. - data_logy - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars and the y-axis on a log10 scale. - noise_map - Whether to include a 1D plot (via `plot`) of the noise map. - signal_to_noise_map - Whether to include a 1D plot (via `plot`) of the signal-to-noise map. - pre_cti_data - Whether to include a 1D plot (via `plot`) of the pre-cti data. - """ - - region = kwargs.get("region", None) - suffix = f"_{region}" if region is not None else "" - - self._subplot_custom_plot( - data=data, - data_logy=data_logy, - noise_map=noise_map, - signal_to_noise_map=signal_to_noise_map, - pre_cti_data=pre_cti_data, - auto_labels=AutoLabels(filename=f"{auto_filename}{suffix}"), - **kwargs, - ) - - def subplot_dataset(self, region: Optional[str] = None): - """ - Standard subplot of the attributes of the plotter's `Dataset1D` object. - """ - self.subplot( - region=region, - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - ) diff --git a/autocti/dataset_1d/plot/fit_plots.py b/autocti/dataset_1d/plot/fit_plots.py new file mode 100644 index 00000000..68a30bc5 --- /dev/null +++ b/autocti/dataset_1d/plot/fit_plots.py @@ -0,0 +1,213 @@ +from pathlib import Path +from typing import List, Optional + +import numpy as np + +from autoconf.fitsable import hdu_list_for_output_from +from autoarray.plot.utils import ( + subplots, + subplot_save, + conf_subplot_figsize, + tight_layout, +) + +from autocti.dataset_1d.fit import FitDataset1D +from autocti.util import plot_utils + +# The 1D fit quantities plottable via figure_fit: name -> (title, has data errorbars, model overlay) +_QUANTITY_TITLES = { + "data": "Data", + "noise_map": "Noise Map", + "signal_to_noise_map": "Signal To Noise Map", + "pre_cti_data": "Pre CTI Data", + "post_cti_data": "Post CTI Data", + "residual_map": "Residual Map", + "normalized_residual_map": "Normalized Residual Map", + "chi_squared_map": "Chi-Squared Map", +} + + +def _extract(fit: FitDataset1D, array, region: Optional[str]): + return fit.dataset.layout.extract_region_from(array=array, region=region) + + +def figure_fit( + fit: FitDataset1D, + quantity: str, + region: Optional[str] = None, + logy: bool = False, + ax=None, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Plot a 1D quantity of a ``FitDataset1D`` (optionally extracted and binned over a region, e.g. the FPR + or EPER). + + The ``data`` quantity is drawn as an errorbar figure with the model data overlaid in red; all other + quantities are plain line figures. + + Parameters + ---------- + fit + The 1D fit whose quantity is plotted. + quantity + One of {"data", "noise_map", "signal_to_noise_map", "pre_cti_data", "post_cti_data", + "residual_map", "normalized_residual_map", "chi_squared_map"}. + region + The region extracted and binned {"fpr", "eper"}; ``None`` plots the full fit. + logy + Whether the y-axis is log10 scaled. + """ + suffix = f"_{region}" if region is not None else "" + title_str = plot_utils.title_str_from(dataset=fit.dataset, region=region) + _pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t) + + is_data = quantity == "data" + + plot_utils.plot_cti_1d( + y=_extract(fit, getattr(fit, quantity), region), + y_errors=_extract(fit, fit.noise_map, region) if is_data else None, + y_extra=_extract(fit, fit.model_data, region) if is_data else None, + errorbar=is_data, + logy=logy, + should_plot_zero=plot_utils.should_plot_zero_from(region=region), + text_manual_dict=( + plot_utils.text_manual_dict_from(dataset=fit.dataset, region=region) + if is_data + else None + ), + text_manual_dict_y=plot_utils.text_manual_dict_y_from(region=region), + title=_pf( + f"{_QUANTITY_TITLES[quantity]} {title_str}" + (" [log10]" if logy else "") + ), + ylabel="e-" if quantity != "signal_to_noise_map" else "", + ax=ax, + output_path=output_path, + output_filename=f"{quantity}_logy{suffix}" if logy else f"{quantity}{suffix}", + output_format=output_format, + ) + + +def subplot_fit( + fit: FitDataset1D, + region: Optional[str] = None, + output_path=None, + output_format=None, + title_prefix: str = None, +): + """ + Eight-panel subplot of a ``FitDataset1D``: the data (with model overlay), noise map, signal-to-noise + map, pre-CTI data, post-CTI data, residual map, normalized residual map and chi-squared map, each + optionally extracted and binned over a region (e.g. the FPR or EPER). + + Parameters + ---------- + fit + The 1D fit to visualize. + region + The region extracted and binned {"fpr", "eper"}; ``None`` plots the full fit. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + suffix = f"_{region}" if region is not None else "" + + quantities = list(_QUANTITY_TITLES) + + fig, axes = subplots(2, 4, figsize=conf_subplot_figsize(2, 4)) + axes = axes.flatten() + + for i, quantity in enumerate(quantities): + figure_fit( + fit=fit, + quantity=quantity, + region=region, + ax=axes[i], + title_prefix=title_prefix, + ) + + tight_layout() + subplot_save(fig, output_path, f"subplot_fit{suffix}", output_format) + + +def subplot_fit_list( + fit_list: List[FitDataset1D], + quantity: str = "data", + region: Optional[str] = None, + logy: bool = False, + output_path=None, + output_filename: str = None, + output_format=None, + title_prefix: str = None, +): + """ + Combined subplot of one quantity of a list of ``FitDataset1D`` objects (e.g. from a combined + analysis), one panel per fit. + + Parameters + ---------- + fit_list + The list of 1D fits to visualize. + quantity + The fit quantity plotted in every panel (see ``figure_fit``). + region + The region extracted and binned {"fpr", "eper"}; ``None`` plots the full fits. + logy + Whether the y-axes are log10 scaled. + """ + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + suffix = f"_{region}" if region is not None else "" + + n = len(fit_list) + cols = min(n, 3) + rows = (n + cols - 1) // cols + + fig, axes = subplots(rows, cols, figsize=conf_subplot_figsize(rows, cols)) + axes = axes.flatten() if hasattr(axes, "flatten") else [axes] + + for i, fit in enumerate(fit_list): + figure_fit( + fit=fit, + quantity=quantity, + region=region, + logy=logy, + ax=axes[i], + title_prefix=title_prefix, + ) + + for ax in axes[n:]: + ax.axis("off") + + tight_layout() + + if output_filename is None: + output_filename = f"subplot_{quantity}{'_logy' if logy else ''}_list{suffix}" + + subplot_save(fig, output_path, output_filename, output_format) + + +def fits_fit(fit: FitDataset1D, output_path): + """ + Output the quantities of a ``FitDataset1D`` to a single ``fit.fits`` file with one extension per + quantity (model data, residual map, normalized residual map and chi-squared map). + """ + hdu_list = hdu_list_for_output_from( + values_list=[ + np.asarray(fit.model_data.native), + np.asarray(fit.residual_map.native), + np.asarray(fit.normalized_residual_map.native), + np.asarray(fit.chi_squared_map.native), + ], + ext_name_list=[ + "model_data", + "residual_map", + "normalized_residual_map", + "chi_squared_map", + ], + header_dict=fit.dataset.mask.header_dict, + ) + hdu_list.writeto(Path(output_path) / "fit.fits", overwrite=True) diff --git a/autocti/dataset_1d/plot/fit_plotters.py b/autocti/dataset_1d/plot/fit_plotters.py deleted file mode 100644 index 9bd1e0da..00000000 --- a/autocti/dataset_1d/plot/fit_plotters.py +++ /dev/null @@ -1,353 +0,0 @@ -import numpy as np -from typing import Callable, Optional - -import autoarray.plot as aplt - -from autoarray.plot.auto_labels import AutoLabels - -from autocti.plot.abstract_plotters import Plotter -from autocti.dataset_1d.fit import FitDataset1D - - -class FitDataset1DPlotter(Plotter): - def __init__( - self, - fit: FitDataset1D, - mat_plot_1d: aplt.MatPlot1D = aplt.MatPlot1D(), - visuals_1d: aplt.Visuals1D = aplt.Visuals1D(), - include_1d: aplt.Include1D = aplt.Include1D(), - ): - """ - Plots the attributes of `FitDataset1D` objects using the matplotlib method `line()` and many other matplotlib - functions which customize the plot's appearance. - - The `mat_plot_1d` attribute wraps matplotlib function calls to make the figure. By default, the settings - passed to every matplotlib function called are those specified in the `config/visualize/mat_wrap/*.ini` files, - but a user can manually input values into `MatPlot1d` to customize the figure's appearance. - - Overlaid on the figure are visuals, contained in the `Visuals1D` object. Attributes may be extracted from - the `Imaging` and plotted via the visuals object, if the corresponding entry is `True` in the `Include1D` - object or the `config/visualize/include.ini` file. - - Parameters - ---------- - fit - The fit to the dataset of a 1D dataset the plotter plots. - mat_plot_1d - Contains objects which wrap the matplotlib function calls that make 1D plots. - visuals_1d - Contains 1D visuals that can be overlaid on 1D plots. - include_1d - Specifies which attributes of the `ImagingCI` are extracted and plotted as visuals for 1D plots. - """ - self.fit = fit - - super().__init__( - dataset=fit.dataset, - mat_plot_1d=mat_plot_1d, - include_1d=include_1d, - visuals_1d=visuals_1d, - ) - - def get_visuals_1d(self) -> aplt.Visuals1D: - return self.visuals_1d - - @property - def extract_region_from(self) -> Callable: - return self.fit.dataset.layout.extract_region_from - - def figures_1d( - self, - region: Optional[str] = None, - data: bool = False, - data_logy: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - post_cti_data: bool = False, - residual_map: bool = False, - residual_map_logy: bool = False, - normalized_residual_map: bool = False, - chi_squared_map: bool = False, - ): - """ - Plots the individual attributes of the plotter's `FitDataset1D` object in 1D. - - The API is such that every plottable attribute of the `FitDataset1D` object is an input parameter of type bool - of the function, which if switched to `True` means that it is plotted. - - Parameters - ---------- - region - The region on the 1D dataset where data is extracted and binned {fpr", "eper"} - data - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars. - data_logy - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars and the y-axis on a log10 scale. - noise_map - Whether to make a 1D plot (via `plot`) of the noise map. - signal_to_noise_map - Whether to make a 1D plot (via `plot`) of the signal-to-noise map. - pre_cti_data - Whether to make a 1D plot (via `plot`) of the pre-cti data. - post_cti_data - Whether to make a 1D plot (via `plot`) of the post-cti data. - residual_map - Whether to make a 1D plot (via `plot`) of the residual map, with the noise-map values included as error - bars. - residual_map_logy - Whether to make a 1D plot (via `plot`) of the residual map, with the noise-map values included as error - bars and the y-axis on a log10 scale. - normalized_residual_map - Whether to make a 1D plot (via `plot`) of the normalized residual map. - chi_squared_map - Whether to make a 1D plot (via `plot`) of the chi-squared map. - """ - - suffix = f"_{region}" if region is not None else "" - title_str = self.title_str_from(region=region) - - y_errors = self.extract_region_from(array=self.fit.noise_map, region=region) - y_extra = self.extract_region_from(array=self.fit.model_data, region=region) - - should_plot_zero = self.should_plot_zero_from(region=region) - - if data: - y = self.extract_region_from(array=self.fit.data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar", - y_errors=y_errors, - y_extra=y_extra, - should_plot_zero=should_plot_zero, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data {title_str}", - yunit="e-", - filename=f"data{suffix}", - ), - ) - - if data_logy: - y = self.extract_region_from(array=self.fit.data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar_logy", - y_errors=y_errors, - y_extra=y_extra, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Data {title_str} [log10]", - yunit="e-", - filename=f"data_logy{suffix}", - ), - ) - - if noise_map: - y = self.extract_region_from(array=self.fit.noise_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title="Noise-Map", - yunit="e-", - filename=f"noise_map{suffix}", - ), - ) - - if signal_to_noise_map: - y = self.extract_region_from( - array=self.fit.signal_to_noise_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title="Signal-To-Noise Map", - yunit="", - filename=f"signal_to_noise_map{suffix}", - ), - ) - - if residual_map: - y = self.extract_region_from(array=self.fit.residual_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar", - y_errors=y_errors, - should_plot_zero=should_plot_zero, - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Residual Map {title_str}", - yunit="e-", - filename=f"residual_map{suffix}", - ), - ) - - if residual_map_logy: - y = self.extract_region_from(array=self.fit.residual_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - plot_axis_type_override="errorbar_logy", - y_errors=y_errors, - y_extra=1.0001 * np.zeros(shape=y.shape), - text_manual_dict=self.text_manual_dict_from(region=region), - text_manual_dict_y=self.text_manual_dict_y_from(region=region), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title=f"Residual Map {title_str}", - ylabel="e-", - filename=f"residual_map_logy{suffix}", - ), - ) - - if normalized_residual_map: - y = self.extract_region_from( - array=self.fit.normalized_residual_map, region=region - ) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title="Normalized Residual Map", - yunit=r"\sigma", - filename=f"normalized_residual_map{suffix}", - ), - ) - - if chi_squared_map: - y = self.extract_region_from(array=self.fit.chi_squared_map, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title="Chi-Squared Map", - yunit=r"\chi^2", - filename=f"chi_squared_map{suffix}", - ), - ) - - if pre_cti_data: - y = self.extract_region_from(array=self.fit.pre_cti_data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title="Pre CTI Data", - yunit="e-", - filename=f"pre_cti_data{suffix}", - ), - ) - - if post_cti_data: - y = self.extract_region_from(array=self.fit.post_cti_data, region=region) - - self.mat_plot_1d.plot_yx( - y=y, - x=range(len(y)), - visuals_1d=self.get_visuals_1d(), - auto_labels=AutoLabels( - title="CI Post CTI Image", - yunit="e-", - filename=f"post_cti_data{suffix}", - ), - ) - - def subplot( - self, - data: bool = False, - noise_map: bool = False, - signal_to_noise_map: bool = False, - pre_cti_data: bool = False, - post_cti_data: bool = False, - residual_map: bool = False, - normalized_residual_map: bool = False, - chi_squared_map: bool = False, - auto_filename="subplot_fit", - **kwargs, - ): - """ - Plots the individual attributes of the plotter's `FitDataset1D` object in 1D on a subplot. - - The API is such that every plottable attribute of the `FitDataset1D` object is an input parameter of type bool - of the function, which if switched to `True` means that it is included on the subplot. - - Parameters - ---------- - data - Whether to make a 1D plot (via `plot`) of the image data extracted and binned over the region, with the - noise-map values included as error bars. - noise_map - Whether to include a 1D plot (via `plot`) of the noise map. - signal_to_noise_map - Whether to include a 1D plot (via `plot`) of the signal-to-noise map. - pre_cti_data - Whether to include a 1D plot (via `plot`) of the pre-cti data. - post_cti_data - Whether to include a 1D plot (via `plot`) of the post-cti data. - residual_map - Whether to include a 1D plot (via `plot`) of the residual map. - normalized_residual_map - Whether to include a 1D plot (via `plot`) of the normalized residual map. - chi_squared_map - Whether to include a 1D plot (via `plot`) of the chi-squared map. - """ - - region = kwargs.get("region", None) - suffix = f"_{region}" if region is not None else "" - - self._subplot_custom_plot( - data=data, - noise_map=noise_map, - signal_to_noise_map=signal_to_noise_map, - pre_cti_data=pre_cti_data, - post_cti_data=post_cti_data, - residual_map=residual_map, - normalized_residual_map=normalized_residual_map, - chi_squared_map=chi_squared_map, - auto_labels=AutoLabels( - yunit="e-", - xlabel="Pixel No.", - filename=f"{auto_filename}{suffix}", - ), - ) - - def subplot_fit(self, region: Optional[str] = None): - """ - Standard subplot of the attributes of the plotter's `FitDataset1D` object. - """ - return self.subplot( - region=region, - data=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - normalized_residual_map=True, - chi_squared_map=True, - ) diff --git a/autocti/model/plotter.py b/autocti/model/plotter.py new file mode 100644 index 00000000..f749c42d --- /dev/null +++ b/autocti/model/plotter.py @@ -0,0 +1,55 @@ +from __future__ import annotations +import os +from pathlib import Path +from typing import List, Union + +from autoconf import conf + + +def setting(section: Union[List[str], str], name: str): + if isinstance(section, str): + return conf.instance["visualize"]["plots"][section][name] + + for sect in reversed(section): + try: + return conf.instance["visualize"]["plots"][sect][name] + except KeyError: + continue + + return conf.instance["visualize"]["plots"][section[0]][name] + + +def plot_setting(section: Union[List[str], str], name: str) -> bool: + return setting(section, name) + + +class Plotter: + def __init__(self, image_path: Union[Path, str], title_prefix: str = None): + """ + Base class for the plotters which output visualization during a model fit. + + The methods of a `Plotter` are called throughout a non-linear search via the `Analysis` and + `Visualizer` classes. + + The images output by a `Plotter` are customized using the file `config/visualize.yaml`. + + Parameters + ---------- + image_path + The path on the hard-disk to the `image` folder of the non-linear search results, where all + visualization is saved. + title_prefix + An optional string prefixed to every plot title. + """ + self.image_path = Path(image_path) + self.title_prefix = title_prefix + + os.makedirs(image_path, exist_ok=True) + + @property + def fmt(self) -> List[str]: + """The output file format(s) read from the `subplot_format` visualize config entry.""" + try: + return conf.instance["visualize"]["plots"]["subplot_format"] + except KeyError: + return "png" diff --git a/autocti/model/plotter_interface.py b/autocti/model/plotter_interface.py deleted file mode 100644 index f337bb3f..00000000 --- a/autocti/model/plotter_interface.py +++ /dev/null @@ -1,33 +0,0 @@ -from os import path -from autoconf import conf -from autoarray.plot.wrap.base import Output -from autoarray.plot.mat_plot.one_d import MatPlot1D -from autoarray.plot.mat_plot.two_d import MatPlot2D -from autoarray.plot.include.one_d import Include1D -from autoarray.plot.include.two_d import Include2D - - -def setting(section, name): - return conf.instance["visualize"]["plots"][section][name] - - -def plot_setting(section, name): - return setting(section, name) - - -class PlotterInterface: - def __init__(self, image_path): - self.image_path = image_path - - self.include_1d = Include1D() - self.include_2d = Include2D() - - def mat_plot_1d_from(self, subfolders, format="png"): - return MatPlot1D( - output=Output(path=path.join(self.image_path, subfolders), format=format) - ) - - def mat_plot_2d_from(self, subfolders, format="png"): - return MatPlot2D( - output=Output(path=path.join(self.image_path, subfolders), format=format) - ) diff --git a/autocti/plot/__init__.py b/autocti/plot/__init__.py index f6ea5002..f840c27e 100644 --- a/autocti/plot/__init__.py +++ b/autocti/plot/__init__.py @@ -1,54 +1,49 @@ -from autofit.non_linear.plot.nest_plotters import NestPlotter -from autofit.non_linear.plot.mcmc_plotters import MCMCPlotter -from autofit.non_linear.plot.mle_plotters import MLEPlotter - -from autoarray.plot.wrap.base import Axis -from autoarray.plot.wrap.base import Units -from autoarray.plot.wrap.base import Figure -from autoarray.plot.wrap.base import Cmap -from autoarray.plot.wrap.base import Colorbar -from autoarray.plot.wrap.base import ColorbarTickParams -from autoarray.plot.wrap.base import TickParams -from autoarray.plot.wrap.base import YTicks -from autoarray.plot.wrap.base import XTicks -from autoarray.plot.wrap.base import Title -from autoarray.plot.wrap.base import YLabel -from autoarray.plot.wrap.base import XLabel -from autoarray.plot.wrap.base import Legend -from autoarray.plot.wrap.base import Output - -from autoarray.plot.wrap.one_d import YXPlot -from autoarray.plot.wrap.two_d import ArrayOverlay -from autoarray.plot.wrap.two_d import GridScatter -from autoarray.plot.wrap.two_d import GridPlot -from autoarray.plot.wrap.two_d import VectorYXQuiver -from autoarray.plot.wrap.two_d import PatchOverlay -from autoarray.plot.wrap.two_d import VoronoiDrawer -from autoarray.plot.wrap.two_d import OriginScatter -from autoarray.plot.wrap.two_d import MaskScatter -from autoarray.plot.wrap.two_d import BorderScatter -from autoarray.plot.wrap.two_d import PositionsScatter -from autoarray.plot.wrap.two_d import IndexScatter -from autoarray.plot.wrap.two_d import MeshGridScatter -from autoarray.plot.wrap.two_d import ParallelOverscanPlot -from autoarray.plot.wrap.two_d import SerialPrescanPlot -from autoarray.plot.wrap.two_d import SerialOverscanPlot - -from autoarray.plot.mat_plot.one_d import MatPlot1D -from autoarray.plot.include.one_d import Include1D -from autoarray.plot.visuals.one_d import Visuals1D -from autoarray.plot.mat_plot.two_d import MatPlot2D -from autoarray.plot.include.two_d import Include2D -from autoarray.plot.visuals.two_d import Visuals2D - -from autoarray.structures.plot.structure_plotters import YX1DPlotter -from autoarray.structures.plot.structure_plotters import YX1DPlotter as Array1DPlotter -from autoarray.structures.plot.structure_plotters import Array2DPlotter - -from autoarray.plot.multi_plotters import MultiFigurePlotter -from autoarray.plot.multi_plotters import MultiYX1DPlotter - -from autocti.dataset_1d.plot.dataset_1d_plotters import Dataset1DPlotter -from autocti.dataset_1d.plot.fit_plotters import FitDataset1DPlotter -from autocti.charge_injection.plot.imaging_ci_plotters import ImagingCIPlotter -from autocti.charge_injection.plot.fit_ci_plotters import FitImagingCIPlotter +from autofit.non_linear.plot import ( + corner_cornerpy, + corner_anesthetic, + subplot_parameters, + log_likelihood_vs_iteration, + output_figure, +) + +from autoarray.plot.array import plot_array +from autoarray.plot.yx import plot_yx +from autoarray.plot.utils import subplot_save + +from autocti.util.plot_utils import plot_cti_1d + +from autocti.dataset_1d.plot.dataset_1d_plots import ( + figure_data as figure_dataset_1d_data, + subplot_dataset as subplot_dataset_1d, + subplot_dataset_list as subplot_dataset_1d_list, +) + +from autocti.dataset_1d.plot.fit_plots import ( + figure_fit as figure_fit_dataset_1d, + subplot_fit as subplot_fit_dataset_1d, + subplot_fit_list as subplot_fit_dataset_1d_list, + fits_fit as fits_fit_dataset_1d, +) + +from autocti.charge_injection.plot.imaging_ci_plots import ( + figure_data_region as figure_imaging_ci_data_region, + figure_pre_cti_data_residual_map, + subplot_dataset as subplot_imaging_ci, + subplot_dataset_region as subplot_imaging_ci_region, + subplot_data_binned as subplot_imaging_ci_data_binned, + subplot_dataset_list as subplot_imaging_ci_list, + subplot_data_region_list as subplot_imaging_ci_data_region_list, +) + +from autocti.charge_injection.plot.fit_ci_plots import ( + figure_fit_region as figure_fit_ci_region, + subplot_fit as subplot_fit_ci, + subplot_fit_region as subplot_fit_ci_region, + subplot_noise_scaling_map_dict, + subplot_fit_list as subplot_fit_ci_list, + subplot_fit_region_list as subplot_fit_ci_region_list, + fits_fit as fits_fit_ci, +) + +from autocti.dataset_1d.model.plotter import PlotterDataset1D +from autocti.charge_injection.model.plotter import PlotterImagingCI diff --git a/autocti/plot/abstract_plotters.py b/autocti/plot/abstract_plotters.py deleted file mode 100644 index 281a91b4..00000000 --- a/autocti/plot/abstract_plotters.py +++ /dev/null @@ -1,152 +0,0 @@ -from typing import Optional - -from autoarray.plot.wrap.base.abstract import set_backend - -set_backend() - -import autoarray.plot as aplt - -from autoarray.plot.abstract_plotters import AbstractPlotter - -from autocti.plot.get_visuals.one_d import GetVisuals1D -from autocti.plot.get_visuals.two_d import GetVisuals2D -from autocti.extract.settings import SettingsExtract -from autocti.mask.mask_2d import Mask2D - -from autocti import exc - - -class Plotter(AbstractPlotter): - def __init__( - self, - dataset, - mat_plot_2d: aplt.MatPlot2D = None, - visuals_2d: aplt.Visuals2D = None, - include_2d: aplt.Include2D = None, - mat_plot_1d: aplt.MatPlot1D = None, - visuals_1d: aplt.Visuals1D = None, - include_1d: aplt.Include1D = None, - ): - self.dataset = dataset - - super().__init__( - mat_plot_2d=mat_plot_2d, - visuals_2d=visuals_2d, - include_2d=include_2d, - mat_plot_1d=mat_plot_1d, - visuals_1d=visuals_1d, - include_1d=include_1d, - ) - - @property - def get_1d(self): - return GetVisuals1D(visuals=self.visuals_1d, include=self.include_1d) - - @property - def get_2d(self): - return GetVisuals2D(visuals=self.visuals_2d, include=self.include_2d) - - def title_str_from(self, region: Optional[str]) -> str: - if self.dataset.settings_dict is not None: - ccd_str = self.dataset.settings_dict.get("CCD") - else: - ccd_str = None - - if region is None: - title_str = "" - elif region == "fpr": - title_str = "FPR" - elif region == "eper": - title_str = "EPER" - elif region == "parallel_fpr": - title_str = "Parallel FPR" - elif region == "parallel_eper": - title_str = "Parallel EPER" - elif region == "serial_fpr": - title_str = "Serial FPR" - elif region == "serial_eper": - title_str = "Serial EPER" - - if ccd_str is None: - return title_str - return f"{ccd_str} {title_str}" - - def title_str_2d_from(self) -> Optional[str]: - if self.dataset.settings_dict is not None: - ccd_str = self.dataset.settings_dict.get("CCD") - ig1_str = self.dataset.settings_dict.get("CI_IG1") - ig2_str = self.dataset.settings_dict.get("CI_IG2") - id_delay_str = self.dataset.settings_dict.get("CI_IDDLY") - - return f"{ccd_str} IG1={ig1_str} IG2={ig2_str} IDD={id_delay_str}" - - def text_manual_dict_from(self, region: Optional[str] = None): - try: - fpr_value = self.dataset.fpr_value - except AttributeError: - fpr_value = None - - text_manual_dict = {} - - if region is not None: - if fpr_value is not None and "eper" in region: - fpr_dict = {"FPR (e-)": self.dataset.fpr_value} - text_manual_dict = {**text_manual_dict, **fpr_dict} - - if self.dataset.settings_dict is not None: - text_manual_dict = {**text_manual_dict, **self.dataset.settings_dict} - - return text_manual_dict - - def text_manual_dict_y_from(self, region: Optional[str] = None): - if region is None or "eper" in region: - return 0.94 - return 0.34 - - def should_plot_zero_from(self, region: Optional[str]): - if region is None: - return False - - if "eper" in region: - return True - - return False - - def fpr_mask_from(self) -> Mask2D: - """ - Returns a mask for the FPR of the dataset, where the prescan and overscan regions are also masked. - - This is used for plotting images binned across the rows and columsn (e.g. the parallel and serial direction). - - This mask means these plots can be made without the FP being visible. - """ - fpr_size = self.dataset.layout.parallel_rows_within_regions[0] - - if any( - [ - fpr_size != fpr_size_of_row - for fpr_size_of_row in self.dataset.layout.parallel_rows_within_regions - ] - ): - raise exc.PlottingException( - "The FPR in this dataset have a variable number of rows. This means that masknig the FPR in the" - "figures_1d_data_binned method is not supported." - ) - - fpr_mask = self.dataset.layout.extract.parallel_fpr.mask_from( - settings=SettingsExtract(pixels=(0, fpr_size)), - pixel_scales=self.dataset.pixel_scales, - ) - - serial_prescan = self.dataset.layout.extract.serial_prescan.serial_prescan - fpr_mask[ - serial_prescan.y0 : serial_prescan.y1, serial_prescan.x0 : serial_prescan.x1 - ] = True - - serial_overscan = self.dataset.layout.extract.serial_overscan.serial_overscan - fpr_mask[ - serial_overscan.y0 : serial_overscan.y1, - serial_overscan.x0 : serial_overscan.x1, - ] = True - - return fpr_mask diff --git a/autocti/plot/get_visuals/one_d.py b/autocti/plot/get_visuals/one_d.py deleted file mode 100644 index 27412e7d..00000000 --- a/autocti/plot/get_visuals/one_d.py +++ /dev/null @@ -1,24 +0,0 @@ -import autoarray.plot as aplt - - -class GetVisuals1D(aplt.GetVisuals1D): - def __init__(self, include: aplt.Include1D, visuals: aplt.Visuals1D): - """ - Class which gets 1D attributes and adds them to a `Visuals1D` objects, such that they are plotted on 1D figures. - - For a visual to be extracted and added for plotting, it must have a `True` value in its corresponding entry in - the `Include1D` object. If this entry is `False`, the `GetVisuals1D.get` method returns a None and the attribute - is omitted from the plot. - - The `GetVisuals1D` class adds new visuals to a pre-existing `Visuals1D` object that is passed to its `__init__` - method. This only adds a new entry if the visual are not already in this object. - - Parameters - ---------- - include - Sets which 1D visuals are included on the figure that is to be plotted (only entries which are `True` - are extracted via the `GetVisuals1D` object). - visuals - The pre-existing visuals of the plotter which new visuals are added too via the `GetVisuals1D` class. - """ - super().__init__(include=include, visuals=visuals) diff --git a/autocti/plot/get_visuals/two_d.py b/autocti/plot/get_visuals/two_d.py deleted file mode 100644 index 4c28fae5..00000000 --- a/autocti/plot/get_visuals/two_d.py +++ /dev/null @@ -1,70 +0,0 @@ -import autoarray.plot as aplt - -from autocti.charge_injection.fit import FitImagingCI - - -class GetVisuals2D(aplt.GetVisuals2D): - def __init__(self, include: aplt.Include2D, visuals: aplt.Visuals2D): - """ - Class which gets 2D attributes and adds them to a `Visuals2D` objects, such that they are plotted on 2D figures. - - For a visual to be extracted and added for plotting, it must have a `True` value in its corresponding entry in - the `Include2D` object. If this entry is `False`, the `GetVisuals2D.get` method returns a None and the - attribute is omitted from the plot. - - The `GetVisuals2D` class adds new visuals to a pre-existing `Visuals2D` object that is passed to - its `__init__` method. This only adds a new entry if the visual are not already in this object. - - Parameters - ---------- - include - Sets which 2D visuals are included on the figure that is to be plotted (only entries which are `True` - are extracted via the `GetVisuals2D` object). - visuals - The pre-existing visuals of the plotter which new visuals are added too via the `GetVisuals2D` class. - """ - super().__init__(include=include, visuals=visuals) - - def via_fit_imaging_ci_from(self, fit: FitImagingCI) -> aplt.Visuals2D: - """ - From a `FitImagingCI` get its attributes that can be plotted and return them in a `Visuals2D` object. - - Only attributes not already in `self.visuals` and with `True` entries in the `Include2D` object are extracted - for plotting. - - From a `FitImagingCI` the following attributes can be extracted for plotting: - - - origin: the (y,x) origin of the 2D coordinate system. - - mask: the 2D mask. - - border: the border of the 2D mask, which are all of the mask's exterior edge pixels. - - parallel overscan: the 2D region defining the parallel overscan on the imaging data. - - serial prescan: the 2D region defining the serial prerscan on the imaging data. - - serial overscan: the 2D region defining the serial overscan on the imaging data. - - Parameters - ---------- - fit - The fit imaging object whose attributes are extracted for plotting. - - Returns - ------- - Visuals2D - The collection of attributes that are plotted by a `Plotter` object. - """ - visuals_2d_via_fit = super().via_fit_imaging_from(fit=fit) - - parallel_overscan = ( - self.get("parallel_overscan", fit.dataset.layout.parallel_overscan), - ) - serial_prescan = ( - self.get("serial_prescan", fit.dataset.layout.serial_prescan), - ) - serial_overscan = ( - self.get("serial_overscan", fit.dataset.layout.serial_overscan), - ) - - return visuals_2d_via_fit + self.visuals.__class__( - parallel_overscan=parallel_overscan, - serial_prescan=serial_prescan, - serial_overscan=serial_overscan, - ) diff --git a/autocti/util/plot_utils.py b/autocti/util/plot_utils.py new file mode 100644 index 00000000..94ec270d --- /dev/null +++ b/autocti/util/plot_utils.py @@ -0,0 +1,254 @@ +import logging +from typing import Dict, Optional + +import numpy as np + +from autoarray.plot.utils import subplot_save + +from autocti.extract.settings import SettingsExtract +from autocti.mask.mask_2d import Mask2D + +from autocti import exc + +logger = logging.getLogger(__name__) + + +def title_str_from(dataset, region: Optional[str]) -> str: + """ + The title of a 1D CTI figure, which describes the region of the dataset that is extracted and binned + (e.g. the FPR or EPER) and, when available, the CCD identifier from the dataset's settings dictionary. + + Parameters + ---------- + dataset + The dataset (e.g. `Dataset1D`, `ImagingCI`) whose `settings_dict` may contain a CCD identifier. + region + The region string {"fpr", "eper", "parallel_fpr", "parallel_eper", "serial_fpr", "serial_eper"} or None. + """ + if dataset.settings_dict is not None: + ccd_str = dataset.settings_dict.get("CCD") + else: + ccd_str = None + + title_str = { + None: "", + "fpr": "FPR", + "eper": "EPER", + "parallel_fpr": "Parallel FPR", + "parallel_eper": "Parallel EPER", + "serial_fpr": "Serial FPR", + "serial_eper": "Serial EPER", + "fpr_non_uniformity": "FPR Non Uniformity", + }.get(region, region or "") + + if ccd_str is None: + return title_str + return f"{ccd_str} {title_str}" + + +def title_str_2d_from(dataset) -> Optional[str]: + """ + The title of a 2D CTI figure, built from the charge injection electronics settings in the dataset's + settings dictionary (CCD id, injection gate voltages, id delay) when available. + """ + if dataset.settings_dict is not None: + ccd_str = dataset.settings_dict.get("CCD") + ig1_str = dataset.settings_dict.get("CI_IG1") + ig2_str = dataset.settings_dict.get("CI_IG2") + id_delay_str = dataset.settings_dict.get("CI_IDDLY") + + return f"{ccd_str} IG1={ig1_str} IG2={ig2_str} IDD={id_delay_str}" + + +def text_manual_dict_from(dataset, region: Optional[str] = None) -> Dict: + """ + A dictionary of text annotations added to 1D CTI figures — the dataset's FPR value (on EPER figures, + where the trailed charge is interpreted relative to the injection level) and its settings dictionary. + """ + try: + fpr_value = dataset.fpr_value + except AttributeError: + fpr_value = None + + text_manual_dict = {} + + if region is not None: + if fpr_value is not None and "eper" in region: + text_manual_dict = {**text_manual_dict, "FPR (e-)": dataset.fpr_value} + + if dataset.settings_dict is not None: + text_manual_dict = {**text_manual_dict, **dataset.settings_dict} + + return text_manual_dict + + +def text_manual_dict_y_from(region: Optional[str] = None) -> float: + """ + The figure-fraction y coordinate where the text annotations of a 1D CTI figure begin. + """ + if region is None or "eper" in region: + return 0.94 + return 0.34 + + +def should_plot_zero_from(region: Optional[str]) -> bool: + """ + Whether a 1D CTI figure includes a horizontal line at y=0 — EPER trails decay towards zero, so the + zero line aids interpretation. + """ + if region is None: + return False + + return "eper" in region + + +def fpr_mask_from(dataset) -> Mask2D: + """ + Returns a mask of a charge injection dataset's FPRs, where the serial prescan and overscan regions are + also masked. + + This is used for plotting images binned across rows and columns (e.g. the parallel and serial directions) + with the FPR excluded. + """ + fpr_size = dataset.layout.parallel_rows_within_regions[0] + + if any( + [ + fpr_size != fpr_size_of_row + for fpr_size_of_row in dataset.layout.parallel_rows_within_regions + ] + ): + raise exc.PlottingException( + "The FPR in this dataset have a variable number of rows. This means that masking the FPR in " + "data-binned figures is not supported." + ) + + fpr_mask = dataset.layout.extract.parallel_fpr.mask_from( + settings=SettingsExtract(pixels=(0, fpr_size)), + pixel_scales=dataset.pixel_scales, + ) + + serial_prescan = dataset.layout.extract.serial_prescan.serial_prescan + 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 + + return fpr_mask + + +def plot_cti_1d( + y, + y_errors=None, + y_extra=None, + y_extra_color: str = "r", + logy: bool = False, + errorbar: bool = False, + ls_errorbar: str = "", + title: str = "", + ylabel: str = "e-", + xlabel: str = "Pixel No.", + text_manual_dict: Optional[Dict] = None, + text_manual_dict_y: float = 0.94, + should_plot_zero: bool = False, + ax=None, + output_path=None, + output_filename: str = "cti_1d", + output_format=None, +): + """ + Plot a 1D quantity of a CTI dataset or fit (e.g. the data binned over an FPR or EPER region) using + direct matplotlib calls. + + This is the 1D plotting primitive of PyAutoCTI, which every ``*_plots.py`` figure and subplot panel + goes through. When ``ax`` is input the line is drawn on the existing axes (subplot panel); otherwise a + new figure is created and output via ``autoarray.plot.utils.subplot_save``. + + Parameters + ---------- + y + The 1D array of values plotted on the y-axis. + y_errors + The noise values of every y value, plotted via ``errorbar`` when ``errorbar=True``. + y_extra + An optional second y series overlaid as a line (e.g. the model data of a fit). + logy + Whether the y-axis is log10 scaled (used for EPER trails which span decades). + errorbar + Whether the primary series is drawn via ``plt.errorbar`` with ``y_errors``. + ls_errorbar + The linestyle of the errorbar plot (e.g. "-" to connect points, "" for markers only). + text_manual_dict + A dict of ``label: value`` annotations written down the side of the figure (e.g. electronics + settings, the FPR value on EPER figures). + text_manual_dict_y + The axes-fraction y coordinate where annotations start. + should_plot_zero + Whether a horizontal line at y=0 is drawn (EPER figures). + ax + Existing matplotlib axes to draw onto; ``None`` creates and saves/shows a new figure. + """ + import matplotlib.pyplot as plt + + if isinstance(output_format, (list, tuple)): + output_format = output_format[0] + + standalone = ax is None + + if standalone: + from autoarray.plot.utils import conf_figsize + + fig, ax = plt.subplots(figsize=conf_figsize()) + else: + fig = ax.figure + + y = np.asarray(y) + x = np.arange(len(y)) + + if errorbar: + ax.errorbar( + x, + y, + yerr=np.asarray(y_errors) if y_errors is not None else None, + color="k", + ecolor="k", + elinewidth=1, + capsize=2, + linestyle=ls_errorbar, + marker=".", + ) + else: + ax.plot(x, y, color="k") + + if y_extra is not None: + ax.plot(x, np.asarray(y_extra), color=y_extra_color) + + if logy: + ax.set_yscale("log") + + if should_plot_zero and not logy: + ax.axhline(y=0.0, color="b", linestyle="--", linewidth=1) + + ax.set_title(title) + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + + if text_manual_dict: + text_y = text_manual_dict_y + for key, value in text_manual_dict.items(): + ax.text( + 0.7, + text_y, + f"{key} = {value}", + transform=ax.transAxes, + fontsize=8, + ) + text_y -= 0.05 + + if standalone: + subplot_save(fig, output_path, output_filename, output_format) diff --git a/test_autocti/charge_injection/model/test_plotter_imaging_ci.py b/test_autocti/charge_injection/model/test_plotter_imaging_ci.py new file mode 100644 index 00000000..8b57340a --- /dev/null +++ b/test_autocti/charge_injection/model/test_plotter_imaging_ci.py @@ -0,0 +1,109 @@ +import shutil +from pathlib import Path + +import pytest + +from autocti.charge_injection.model.plotter import PlotterImagingCI + +directory = Path(__file__).resolve().parent + + +@pytest.fixture(name="plot_path") +def make_plot_path(): + return directory / "files" + + +def test__dataset(imaging_ci_7x7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterImagingCI(image_path=plot_path) + + plotter.dataset(dataset=imaging_ci_7x7) + plotter.dataset_regions( + dataset=imaging_ci_7x7, region_list=["parallel_fpr", "serial_eper"] + ) + + assert str(Path(plot_path) / "dataset" / "subplot_dataset.png") in plot_patch.paths + assert ( + str(Path(plot_path) / "dataset" / "subplot_1d_ci_parallel_fpr.png") + in plot_patch.paths + ) + assert str(Path(plot_path) / "dataset" / "data_serial_eper.png") in plot_patch.paths + + +def test__dataset_combined(imaging_ci_7x7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterImagingCI(image_path=plot_path) + + plotter.dataset_combined(dataset_list=[imaging_ci_7x7, imaging_ci_7x7]) + plotter.dataset_regions_combined( + dataset_list=[imaging_ci_7x7, imaging_ci_7x7], region_list=["parallel_fpr"] + ) + + assert ( + str(Path(plot_path) / "dataset_combined" / "subplot_dataset_list.png") + in plot_patch.paths + ) + assert ( + str(Path(plot_path) / "dataset_combined" / "subplot_data_list_parallel_fpr.png") + in plot_patch.paths + ) + + +def test__fit(fit_ci_7x7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterImagingCI(image_path=plot_path) + + plotter.fit(fit=fit_ci_7x7, during_analysis=True) + plotter.fit_1d_regions( + fit=fit_ci_7x7, region_list=["parallel_fpr"], during_analysis=True + ) + + assert str(Path(plot_path) / "fit_dataset" / "subplot_fit.png") in plot_patch.paths + assert ( + str(Path(plot_path) / "fit_dataset" / "subplot_1d_fit_ci_parallel_fpr.png") + in plot_patch.paths + ) + + +def test__fit__not_during_analysis_outputs_fits(fit_ci_7x7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterImagingCI(image_path=plot_path) + + plotter.fit(fit=fit_ci_7x7, during_analysis=False) + + assert (Path(plot_path) / "fit_dataset" / "fit.fits").exists() + + +def test__fit_combined(fit_ci_7x7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterImagingCI(image_path=plot_path) + + plotter.fit_combined(fit_list=[fit_ci_7x7, fit_ci_7x7], during_analysis=True) + plotter.fit_1d_regions_combined( + fit_list=[fit_ci_7x7, fit_ci_7x7], + region_list=["parallel_eper"], + during_analysis=True, + ) + + assert ( + str(Path(plot_path) / "fit_dataset_combined" / "subplot_residual_map_list.png") + in plot_patch.paths + ) + assert ( + str( + Path(plot_path) + / "fit_dataset_combined" + / "subplot_data_list_parallel_eper.png" + ) + in plot_patch.paths + ) diff --git a/test_autocti/charge_injection/model/test_plotter_interface_ci.py b/test_autocti/charge_injection/model/test_plotter_interface_ci.py deleted file mode 100644 index ec5fc5b5..00000000 --- a/test_autocti/charge_injection/model/test_plotter_interface_ci.py +++ /dev/null @@ -1,134 +0,0 @@ -import os -import shutil -from os import path - -import pytest -from autocti.charge_injection.model.plotter_interface import PlotterInterfaceImagingCI - -directory = path.dirname(path.abspath(__file__)) - - -@pytest.fixture(name="plot_path") -def make_visualizer_plotter_setup(): - return path.join("{}".format(directory), "files") - - -def test__dataset(imaging_ci_7x7, plot_path, plot_patch): - if path.exists(plot_path): - shutil.rmtree(plot_path) - - imaging_ci_7x7.cosmic_ray_map[0, 0] = 1 - - visualizer = PlotterInterfaceImagingCI(image_path=plot_path) - - visualizer.dataset(dataset=imaging_ci_7x7) - - plot_path = path.join(plot_path, "dataset") - - assert path.join(plot_path, "subplot_dataset.png") in plot_patch.paths - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - - -def test__dataset_regions(imaging_ci_7x7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceImagingCI(image_path=plot_path) - - visualizer.dataset_regions(dataset=imaging_ci_7x7, region_list=["parallel_fpr"]) - - plot_path = path.join(plot_path, "dataset") - - assert path.join(plot_path, "subplot_1d_ci_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_parallel_fpr.png") not in plot_patch.paths - - -def test__fit_ci(fit_ci_7x7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceImagingCI(image_path=plot_path) - - visualizer.fit(fit=fit_ci_7x7, during_analysis=True) - - plot_path = path.join(plot_path, "fit_dataset") - - assert path.join(plot_path, "subplot_fit.png") in plot_patch.paths - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - - plot_patch.paths = [] - - visualizer.fit(fit=fit_ci_7x7, during_analysis=False) - - assert path.join(plot_path, "subplot_fit.png") in plot_patch.paths - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") in plot_patch.paths - - -def test__fit_ci_regions(fit_ci_7x7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceImagingCI(image_path=plot_path) - - visualizer.fit_1d_regions( - fit=fit_ci_7x7, region_list=["parallel_fpr"], during_analysis=True - ) - - plot_path = path.join(plot_path, "fit_dataset") - - assert ( - path.join(plot_path, "subplot_1d_fit_ci_parallel_fpr.png") in plot_patch.paths - ) - assert path.join(plot_path, "data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_parallel_fpr.png") not in plot_patch.paths - - plot_patch.paths = [] - - visualizer.fit_1d_regions( - fit=fit_ci_7x7, region_list=["parallel_fpr"], during_analysis=False - ) - - assert ( - path.join(plot_path, "subplot_1d_fit_ci_parallel_fpr.png") in plot_patch.paths - ) - assert path.join(plot_path, "data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_parallel_fpr.png") not in plot_patch.paths - - -def test__fit_combined(fit_ci_7x7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceImagingCI(image_path=plot_path) - - visualizer.fit_combined(fit_list=[fit_ci_7x7, fit_ci_7x7], during_analysis=True) - - plot_path = path.join(plot_path, "fit_dataset_combined") - - assert path.join(plot_path, "subplot_residual_map.png") in plot_patch.paths - assert path.join(plot_path, "subplot_chi_squared_map.png") not in plot_patch.paths - - -def test__fit_regions_combined(fit_ci_7x7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceImagingCI(image_path=plot_path) - - visualizer.fit_1d_regions_combined( - fit_list=[fit_ci_7x7, fit_ci_7x7], - region_list=["parallel_fpr"], - during_analysis=True, - ) - - plot_path = path.join(plot_path, "fit_dataset_combined") - - assert path.join(plot_path, "subplot_data_parallel_fpr.png") in plot_patch.paths - assert ( - path.join(plot_path, "subplot_data_logy_parallel_fpr.png") - not in plot_patch.paths - ) diff --git a/autocti/plot/get_visuals/__init__.py b/test_autocti/charge_injection/plot/__init__.py similarity index 100% rename from autocti/plot/get_visuals/__init__.py rename to test_autocti/charge_injection/plot/__init__.py diff --git a/test_autocti/charge_injection/plot/test_fit_ci_plots.py b/test_autocti/charge_injection/plot/test_fit_ci_plots.py new file mode 100644 index 00000000..fcc24d6d --- /dev/null +++ b/test_autocti/charge_injection/plot/test_fit_ci_plots.py @@ -0,0 +1,92 @@ +from pathlib import Path + +import pytest + +import autocti.plot as aplt + +directory = Path(__file__).resolve().parent + + +@pytest.fixture(name="plot_path") +def make_plot_path(): + return directory / "files" / "plots" + + +def test__figure_fit_region(fit_ci_7x7, plot_path, plot_patch): + aplt.figure_fit_ci_region( + fit=fit_ci_7x7, + quantity="data", + region="parallel_fpr", + output_path=plot_path, + output_format="png", + ) + aplt.figure_fit_ci_region( + fit=fit_ci_7x7, + quantity="residual_map", + region="parallel_eper", + logy=True, + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "data_parallel_fpr.png") in plot_patch.paths + assert ( + str(Path(plot_path) / "residual_map_logy_parallel_eper.png") in plot_patch.paths + ) + + +def test__subplot_fit(fit_ci_7x7, plot_path, plot_patch): + aplt.subplot_fit_ci(fit=fit_ci_7x7, output_path=plot_path, output_format="png") + + assert str(Path(plot_path) / "subplot_fit.png") in plot_patch.paths + + +def test__subplot_fit_region(fit_ci_7x7, plot_path, plot_patch): + aplt.subplot_fit_ci_region( + fit=fit_ci_7x7, + region="serial_fpr", + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "subplot_1d_fit_ci_serial_fpr.png") in plot_patch.paths + + +def test__subplot_noise_scaling_map_dict(fit_ci_7x7, plot_path, plot_patch): + aplt.subplot_noise_scaling_map_dict( + fit=fit_ci_7x7, output_path=plot_path, output_format="png" + ) + + assert ( + str(Path(plot_path) / "subplot_noise_scaling_map_dict.png") in plot_patch.paths + ) + + +def test__subplot_fit_list(fit_ci_7x7, plot_path, plot_patch): + aplt.subplot_fit_ci_list( + fit_list=[fit_ci_7x7, fit_ci_7x7], + quantity="chi_squared_map", + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "subplot_chi_squared_map_list.png") in plot_patch.paths + + +def test__subplot_fit_region_list(fit_ci_7x7, plot_path, plot_patch): + aplt.subplot_fit_ci_region_list( + fit_list=[fit_ci_7x7, fit_ci_7x7], + region="parallel_eper", + output_path=plot_path, + output_format="png", + ) + + assert ( + str(Path(plot_path) / "subplot_data_list_parallel_eper.png") in plot_patch.paths + ) + + +def test__fits_fit(fit_ci_7x7, tmp_path): + aplt.fits_fit_ci(fit=fit_ci_7x7, output_path=tmp_path) + + assert (Path(tmp_path) / "fit.fits").exists() diff --git a/test_autocti/charge_injection/plot/test_fit_ci_plotters.py b/test_autocti/charge_injection/plot/test_fit_ci_plotters.py deleted file mode 100644 index 404daf98..00000000 --- a/test_autocti/charge_injection/plot/test_fit_ci_plotters.py +++ /dev/null @@ -1,191 +0,0 @@ -import copy -from os import path -import pytest -from autocti import plot as aplt - -directory = path.dirname(path.realpath(__file__)) - - -@pytest.fixture(name="plot_path") -def make_fit_plotter_setup(): - return path.join( - "{}".format(path.dirname(path.realpath(__file__))), "files", "plots", "fit" - ) - - -def test__individual_attribute_plots__all_plot_correctly( - fit_ci_7x7, plot_path, plot_patch -): - fit_plotter = aplt.FitImagingCIPlotter( - fit=fit_ci_7x7, - mat_plot_2d=aplt.MatPlot2D(output=aplt.Output(plot_path, format="png")), - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - fit_plotter.figures_2d( - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - residual_map=True, - normalized_residual_map=True, - chi_squared_map=True, - ) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "post_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "residual_map.png") in plot_patch.paths - assert path.join(plot_path, "normalized_residual_map.png") in plot_patch.paths - assert path.join(plot_path, "chi_squared_map.png") in plot_patch.paths - - plot_patch.paths = [] - - fit_plotter.figures_2d( - data=True, - noise_map=False, - signal_to_noise_map=False, - pre_cti_data=True, - post_cti_data=True, - chi_squared_map=True, - ) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") not in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "post_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "residual_map.png") not in plot_patch.paths - assert path.join(plot_path, "chi_squared_map.png") in plot_patch.paths - - -def test__individual_line_attriutes_plot__all_plot_correctly_output( - fit_ci_7x7, plot_path, plot_patch -): - fit_plotter = aplt.FitImagingCIPlotter( - fit=fit_ci_7x7, - mat_plot_2d=aplt.MatPlot2D(output=aplt.Output(plot_path, format="png")), - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - fit_plotter.figures_1d( - region="parallel_fpr", - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - residual_map=True, - normalized_residual_map=True, - chi_squared_map=True, - ) - - assert path.join(plot_path, "data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_parallel_fpr.png") in plot_patch.paths - assert ( - path.join(plot_path, "signal_to_noise_map_parallel_fpr.png") in plot_patch.paths - ) - assert path.join(plot_path, "pre_cti_data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "post_cti_data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "residual_map_parallel_fpr.png") in plot_patch.paths - assert ( - path.join(plot_path, "normalized_residual_map_parallel_fpr.png") - in plot_patch.paths - ) - assert path.join(plot_path, "chi_squared_map_parallel_fpr.png") in plot_patch.paths - - plot_patch.paths = [] - - fit_plotter.figures_1d( - region="parallel_fpr", - data=True, - noise_map=False, - signal_to_noise_map=False, - pre_cti_data=True, - post_cti_data=True, - chi_squared_map=True, - ) - - assert path.join(plot_path, "data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_parallel_fpr.png") not in plot_patch.paths - assert ( - path.join(plot_path, "signal_to_noise_map_parallel_fpr.png") - not in plot_patch.paths - ) - assert path.join(plot_path, "pre_cti_data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "post_cti_data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "residual_map_parallel_fpr.png") not in plot_patch.paths - assert path.join(plot_path, "chi_squared_map_parallel_fpr.png") in plot_patch.paths - - -def test__figures_1d_data_binned(fit_ci_7x7, plot_path, plot_patch): - fit_plotter = aplt.FitImagingCIPlotter( - fit=fit_ci_7x7, - mat_plot_2d=aplt.MatPlot2D(output=aplt.Output(plot_path, format="png")), - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - fit_plotter.figures_1d_data_binned( - rows_fpr=True, - rows_no_fpr=True, - columns_fpr=True, - columns_no_fpr=True, - ) - - assert path.join(plot_path, "fit_data_binned_rows_fpr.png") in plot_patch.paths - assert path.join(plot_path, "fit_data_binned_rows_no_fpr.png") in plot_patch.paths - assert path.join(plot_path, "fit_data_binned_columns_fpr.png") in plot_patch.paths - assert ( - path.join(plot_path, "fit_data_binned_columns_no_fpr.png") in plot_patch.paths - ) - - -def test__fit_ci_subplots_are_output(fit_ci_7x7, plot_path, plot_patch): - fit_plotter = aplt.FitImagingCIPlotter( - fit=fit_ci_7x7, - mat_plot_2d=aplt.MatPlot2D(output=aplt.Output(plot_path, format="png")), - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - fit_plotter.subplot_fit() - assert path.join(plot_path, "subplot_fit.png") in plot_patch.paths - - fit_plotter.subplot_1d(region="parallel_fpr") - assert ( - path.join(plot_path, "subplot_1d_fit_ci_parallel_fpr.png") in plot_patch.paths - ) - - fit_plotter.subplot_noise_scaling_map_dict() - assert ( - path.join(plot_path, "subplot_noise_scaling_map_dict.png") in plot_patch.paths - ) - - -# def test__fit_ci_subplots_lines_are_output(fit_ci_7x7, plot_path, plot_patch): -# -# fit_plotter = aplt.FitImagingCIPlotter(fit=fit_ci_7x7, -# mat_plot_2d=aplt.MatPlot2D(output=aplt.Output(plot_path, format="png")), -# mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), -# ) -# -# fit_plotter.subplot_residual_map_lines( -# region="parallel_fpr", -# ) -# -# assert path.join(plot_path, "subplot_residual_map_lines.png") in plot_patch.paths -# -# fit_plotter.subplot_normalized_residual_map_lines( -# region="parallel_fpr", -# ) -# assert ( -# path.join(plot_path, "subplot_normalized_residual_map_lines.png") -# in plot_patch.paths -# ) -# -# fit_plotter.subplot_chi_squared_map_lines( -# region="parallel_fpr", -# ) -# assert path.join(plot_path, "subplot_chi_squared_map_lines.png") in plot_patch.paths diff --git a/test_autocti/charge_injection/plot/test_imaging_ci_plots.py b/test_autocti/charge_injection/plot/test_imaging_ci_plots.py new file mode 100644 index 00000000..781f5834 --- /dev/null +++ b/test_autocti/charge_injection/plot/test_imaging_ci_plots.py @@ -0,0 +1,73 @@ +from pathlib import Path + +import pytest + +import autocti.plot as aplt + +directory = Path(__file__).resolve().parent + + +@pytest.fixture(name="plot_path") +def make_plot_path(): + return directory / "files" / "plots" + + +def test__figure_data_region(imaging_ci_7x7, plot_path, plot_patch): + aplt.figure_imaging_ci_data_region( + dataset=imaging_ci_7x7, + region="parallel_fpr", + output_path=plot_path, + output_format="png", + ) + aplt.figure_imaging_ci_data_region( + dataset=imaging_ci_7x7, + region="parallel_eper", + logy=True, + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "data_parallel_fpr.png") in plot_patch.paths + assert str(Path(plot_path) / "data_logy_parallel_eper.png") in plot_patch.paths + + +def test__subplot_dataset(imaging_ci_7x7, plot_path, plot_patch): + aplt.subplot_imaging_ci( + dataset=imaging_ci_7x7, output_path=plot_path, output_format="png" + ) + + assert str(Path(plot_path) / "subplot_dataset.png") in plot_patch.paths + + +def test__subplot_dataset_region(imaging_ci_7x7, plot_path, plot_patch): + aplt.subplot_imaging_ci_region( + dataset=imaging_ci_7x7, + region="serial_eper", + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "subplot_1d_ci_serial_eper.png") in plot_patch.paths + + +def test__subplot_dataset_list(imaging_ci_7x7, plot_path, plot_patch): + aplt.subplot_imaging_ci_list( + dataset_list=[imaging_ci_7x7, imaging_ci_7x7], + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "subplot_dataset_list.png") in plot_patch.paths + + +def test__subplot_data_region_list(imaging_ci_7x7, plot_path, plot_patch): + aplt.subplot_imaging_ci_data_region_list( + dataset_list=[imaging_ci_7x7, imaging_ci_7x7], + region="parallel_fpr", + output_path=plot_path, + output_format="png", + ) + + assert ( + str(Path(plot_path) / "subplot_data_list_parallel_fpr.png") in plot_patch.paths + ) diff --git a/test_autocti/charge_injection/plot/test_imaging_ci_plotters.py b/test_autocti/charge_injection/plot/test_imaging_ci_plotters.py deleted file mode 100644 index 45e5364d..00000000 --- a/test_autocti/charge_injection/plot/test_imaging_ci_plotters.py +++ /dev/null @@ -1,121 +0,0 @@ -from os import path - -import pytest - -from autocti import plot as aplt - -directory = path.dirname(path.realpath(__file__)) - - -@pytest.fixture(name="plot_path") -def make_dataset_plotter_setup(): - return path.join( - "{}".format(path.dirname(path.realpath(__file__))), - "files", - "plots", - "imaging_ci", - ) - - -def test__figures_2d__individual_attributes_are_output( - imaging_ci_7x7, plot_path, plot_patch -): - dataset_plotter = aplt.ImagingCIPlotter( - dataset=imaging_ci_7x7, - mat_plot_2d=aplt.MatPlot2D(output=aplt.Output(plot_path, format="png")), - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - imaging_ci_7x7.cosmic_ray_map[0, 0] = 1.0 - - dataset_plotter.figures_2d( - data=True, - noise_map=True, - pre_cti_data=True, - signal_to_noise_map=True, - cosmic_ray_map=True, - ) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") in plot_patch.paths - assert path.join(plot_path, "cosmic_ray_map.png") in plot_patch.paths - - plot_patch.paths = [] - - dataset_plotter.figures_2d(data=True, pre_cti_data=True) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") not in plot_patch.paths - - -def test__figures_1d__individual_1d_of_region_are_output( - imaging_ci_7x7, plot_path, plot_patch -): - dataset_plotter = aplt.ImagingCIPlotter( - dataset=imaging_ci_7x7, - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - dataset_plotter.figures_1d( - region="parallel_fpr", - data=True, - noise_map=True, - pre_cti_data=True, - signal_to_noise_map=True, - ) - - assert path.join(plot_path, "data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "pre_cti_data_parallel_fpr.png") in plot_patch.paths - assert ( - path.join(plot_path, "signal_to_noise_map_parallel_fpr.png") in plot_patch.paths - ) - - plot_patch.paths = [] - - dataset_plotter.figures_1d(region="parallel_fpr", data=True, pre_cti_data=True) - - assert path.join(plot_path, "data_parallel_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_parallel_fpr.png") not in plot_patch.paths - assert path.join(plot_path, "pre_cti_data_parallel_fpr.png") in plot_patch.paths - assert ( - path.join(plot_path, "signal_to_noise_map_parallel_fpr.png") - not in plot_patch.paths - ) - - -def test__figures_1d_data_binned(imaging_ci_7x7, plot_path, plot_patch): - dataset_plotter = aplt.ImagingCIPlotter( - dataset=imaging_ci_7x7, - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - dataset_plotter.figures_1d_data_binned( - rows_fpr=True, - rows_no_fpr=True, - columns_fpr=True, - columns_no_fpr=True, - ) - - assert path.join(plot_path, "data_binned_rows_fpr.png") in plot_patch.paths - assert path.join(plot_path, "data_binned_rows_no_fpr.png") in plot_patch.paths - assert path.join(plot_path, "data_binned_columns_fpr.png") in plot_patch.paths - assert path.join(plot_path, "data_binned_columns_no_fpr.png") in plot_patch.paths - - -def test__subplots__output(imaging_ci_7x7, plot_path, plot_patch): - dataset_plotter = aplt.ImagingCIPlotter( - dataset=imaging_ci_7x7, - mat_plot_2d=aplt.MatPlot2D(output=aplt.Output(plot_path, format="png")), - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - dataset_plotter.subplot_dataset() - assert path.join(plot_path, "subplot_dataset.png") in plot_patch.paths - - dataset_plotter.subplot_1d(region="parallel_fpr") - assert path.join(plot_path, "subplot_1d_ci_parallel_fpr.png") in plot_patch.paths diff --git a/test_autocti/config/visualize.yaml b/test_autocti/config/visualize.yaml index 40158e2c..cc0a345c 100644 --- a/test_autocti/config/visualize.yaml +++ b/test_autocti/config/visualize.yaml @@ -6,30 +6,20 @@ general: symmetric_cmap_value: 100.0 # The vmin and vmax of all pre-cti data residual-maps. subplot_ascending_fpr: true # If True, subplots showing FPR / EPER trails of many datasets are in ascending order of FPR value. plots: + subplot_format: [png] # Output format of all plots, can be png, pdf or both (e.g. [png, pdf]). + combined_only: false # If True, only the combined subplots of multi-dataset analyses are output (no per-dataset visualization). dataset: - data: true - data_logy: false - cosmic_ray_map: true - noise_map: false - noise_scaling_map_dict: false - pre_cti_data: true - signal_to_noise_map: false - subplot_dataset: true + subplot_dataset: true # Plot the subplot of all dataset quantities (2D for charge injection imaging, 1D for Dataset1D)? + subplot_dataset_regions: true # Plot per-region binned 1D subplots (e.g. the parallel/serial FPR and EPER)? + data: true # Plot single 1D figures of the data extracted and binned over each region? + data_logy: true # Plot single 1D figures of the data over each region with a log10 y-axis? + data_binned: true # Plot the data binned over rows / columns with and without the FPR (charge injection only)? + fpr_non_uniformity: false # Include the fpr_non_uniformity region in the per-region plots (charge injection only)? fit: - all_at_end_fits: false - all_at_end_png: true - chi_squared_map: false - data: true - data_logy: false - noise_map: false - noise_scaling_map_dict: false - normalized_residual_map: true - post_cti_data: true - pre_cti_data: true - residual_map: true - signal_to_noise_map: false - subplot_fit: true - other: - stochastic_histogram: false - positions: - image_with_positions: true + subplot_fit: true # Plot the subplot of all fit quantities (e.g. model data, residual-map, chi-squared map)? + subplot_fit_regions: true # Plot per-region binned 1D fit subplots (e.g. the parallel/serial FPR and EPER)? + data: true # Plot single 1D figures of the fit data (with model overlay) over each region? + data_logy: true # Plot single 1D figures of the fit data over each region with a log10 y-axis? + residual_map: true # Plot single 1D figures of the residual map over each region? + residual_map_logy: true # Plot single 1D figures of the residual map over each region with a log10 y-axis? + fits_fit: true # Output a fit.fits file containing the model data, residual map, normalized residual map and chi-squared map? diff --git a/test_autocti/conftest.py b/test_autocti/conftest.py index 26967589..d1d6ae95 100644 --- a/test_autocti/conftest.py +++ b/test_autocti/conftest.py @@ -1,338 +1,332 @@ -import os -from os import path -import pytest -from matplotlib import pyplot - -import autocti as ac -from autofit import conf -from autocti import fixtures - -# The Plotter object stack targets the removed autoarray Plotter API and is -# rewritten on the new matplotlib function API in Phase 1 of the CTI -# resurrection epic (PyAutoCTI#82); its tests are quarantined until then. -collect_ignore_glob = [ - "plot/*", - "*/plot/*", -] -collect_ignore = [ - path.join("dataset_1d", "model", "test_plotter_interface_1d.py"), - path.join("charge_injection", "model", "test_plotter_interface_ci.py"), -] - - -class PlotPatch: - def __init__(self): - self.paths = [] - - def __call__(self, path, *args, **kwargs): - self.paths.append(path) - - -@pytest.fixture(name="plot_patch") -def make_plot_patch(monkeypatch): - plot_patch = PlotPatch() - monkeypatch.setattr(pyplot, "savefig", plot_patch) - return plot_patch - - -directory = path.dirname(path.realpath(__file__)) - - -@pytest.fixture(autouse=True, scope="session") -def remove_logs(): - yield - for d, _, files in os.walk(directory): - for file in files: - if file.endswith(".log"): - os.remove(path.join(d, file)) - - -@pytest.fixture(autouse=True) -def set_config_path(request): - conf.instance.push( - new_path=path.join(directory, "config"), - output_path=path.join(directory, "output"), - ) - - -### Arctic ### - - -@pytest.fixture(name="trap_0") -def make_trap_0(): - return fixtures.make_trap_0() - - -@pytest.fixture(name="trap_1") -def make_trap_1(): - return fixtures.make_trap_1() - - -@pytest.fixture(name="traps_x1") -def make_traps_x1(): - return fixtures.make_traps_x1() - - -@pytest.fixture(name="traps_x2") -def make_traps_x2(): - return fixtures.make_traps_x2() - - -@pytest.fixture(name="ccd") -def make_ccd(): - return fixtures.make_ccd() - - -@pytest.fixture(name="clocker_1d") -def make_clocker_1d(): - return fixtures.make_clocker_1d() - - -@pytest.fixture(name="parallel_clocker_2d") -def make_parallel_clocker(): - return fixtures.make_parallel_clocker_2d() - - -@pytest.fixture(name="serial_clocker_2d") -def make_serial_clocker(): - return fixtures.make_serial_clocker_2d() - - -@pytest.fixture(name="parallel_serial_clocker_2d") -def make_parallel_serial_clocker(): - return fixtures.make_parallel_serial_clocker_2d() - - -### MASK ### - - -@pytest.fixture(name="mask_1d_7_unmasked") -def make_mask_1d_7_unmasked(): - return fixtures.make_mask_1d_7_unmasked() - - -@pytest.fixture(name="mask_2d_7x7_unmasked") -def make_mask_2d_7x7_unmasked(): - return fixtures.make_mask_2d_7x7_unmasked() - - -@pytest.fixture(name="mask_2d_7x7") -def make_mask_2d_7x7(): - return fixtures.make_mask_2d_7x7() - - -### LINES ### - - -@pytest.fixture(name="layout_7") -def make_layout_7(): - return fixtures.make_layout_7() - - -@pytest.fixture(name="data_7") -def make_data_7(): - return fixtures.make_data_7() - - -@pytest.fixture(name="noise_map_7") -def make_noise_map_7(): - return fixtures.make_noise_map_7() - - -@pytest.fixture(name="pre_cti_data_7") -def make_pre_cti_data(): - return fixtures.make_pre_cti_data_7() - - -@pytest.fixture(name="dataset_1d_7") -def make_dataset_1d_7(): - return fixtures.make_dataset_1d_7() - - -@pytest.fixture(name="fit_1d_7") -def make_fit_1d_7(): - return fixtures.make_fit_1d_7() - - -### FRAMES ### - - -@pytest.fixture(name="array") -def make_array(): - return ac.Array1D.no_mask( - values=[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], pixel_scales=1.0 - ) - - -@pytest.fixture(name="image_7x7_native") -def make_image_7x7_native(): - return fixtures.make_image_7x7_native() - - -@pytest.fixture(name="noise_map_7x7_native") -def make_noise_map_7x7_native(): - return fixtures.make_noise_map_7x7_native() - - -### IMAGING ### - - -@pytest.fixture(name="imaging_7x7_frame") -def make_imaging_7x7_frame(): - return fixtures.make_imaging_7x7_frame() - - -### CHARGE INJECTION FRAMES ### - - -@pytest.fixture(name="layout_ci_7x7") -def make_layout_ci_7x7(): - return fixtures.make_layout_ci_7x7() - - -@pytest.fixture(name="ci_image_7x7") -def make_ci_image_7x7(): - return fixtures.make_ci_image_7x7() - - -@pytest.fixture(name="ci_noise_map_7x7") -def make_ci_noise_map_7x7(): - return fixtures.make_ci_noise_map_7x7() - - -@pytest.fixture(name="pre_cti_data_7x7") -def make_pre_cti_data_7x7(): - return fixtures.make_pre_cti_data_7x7() - - -@pytest.fixture(name="ci_cosmic_ray_map_7x7") -def make_ci_cosmic_ray_map_7x7(): - return fixtures.make_ci_cosmic_ray_map_7x7() - - -@pytest.fixture(name="ci_noise_scaling_map_dict_7x7") -def make_ci_noise_scaling_map_dict_7x7(): - return fixtures.make_ci_noise_scaling_map_dict_7x7() - - -### CHARGE INJECTION IMAGING ### - - -@pytest.fixture(name="imaging_ci_7x7") -def make_imaging_ci_7x7(): - return fixtures.make_imaging_ci_7x7() - - -### CHARGE INJECTION FITS ### - - -@pytest.fixture(name="hyper_noise_scalar_dict") -def make_hyper_noise_scalar_dict(): - return fixtures.make_hyper_noise_scalar_dict() - - -@pytest.fixture(name="fit_ci_7x7") -def make_fit_ci_7x7(): - return fixtures.make_fit_ci_7x7() - - -# ### PHASES ### - -from autofit.mapper.model import ModelInstance - - -@pytest.fixture(name="samples_summary_with_result") -def make_samples_summary_with_result(): - return fixtures.make_samples_summary_with_result() - - -@pytest.fixture(name="analysis_imaging_ci_7x7") -def make_analysis_imaging_ci_7x7(): - return fixtures.make_analysis_imaging_ci_7x7() - - -# Datasets - - -@pytest.fixture(name="euclid_data") -def make_euclid_data(): - return fixtures.make_euclid_data() - - -@pytest.fixture(name="acs_ccd") -def make_acs_ccd(): - return fixtures.make_acs_ccd() - - -@pytest.fixture(name="acs_quadrant") -def make_acs_quadrant(): - return fixtures.make_acs_quadrant() - - -# Custom Arrays - - -@pytest.fixture(name="parallel_array") -def make_parallel_array(): - return ac.Array2D.no_mask( - values=[ - [0.0, 0.0, 0.0], - [1.0, 1.0, 1.0], # <- Front edge . - [2.0, 2.0, 2.0], # <- Next front edge row. - [3.0, 3.0, 3.0], - [4.0, 4.0, 4.0], - [5.0, 5.0, 5.0], - [6.0, 6.0, 6.0], - [7.0, 7.0, 7.0], - [8.0, 8.0, 8.0], - [9.0, 9.0, 9.0], - ], - pixel_scales=1.0, - ) - - -@pytest.fixture(name="parallel_masked_array") -def make_parallel_masked_array(parallel_array): - mask = ac.Mask2D( - mask=[ - [False, False, False], - [False, False, False], - [False, True, False], - [False, False, True], - [False, False, False], - [False, False, False], - [False, False, False], - [True, False, False], - [False, False, False], - [False, False, False], - ], - pixel_scales=1.0, - ) - - return ac.Array2D(values=parallel_array.native, mask=mask) - - -@pytest.fixture(name="serial_array") -def make_serial_array(): - return ac.Array2D.no_mask( - values=[ - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], - ], - pixel_scales=1.0, - ) - - -@pytest.fixture(name="serial_masked_array") -def make_serial_masked_array(serial_array): - mask = ac.Mask2D( - mask=[ - [False, False, False, False, False, True, False, False, False, False], - [False, False, True, False, False, False, True, False, False, False], - [False, False, False, False, False, False, False, True, False, False], - ], - pixel_scales=1.0, - ) - - return ac.Array2D(values=serial_array.native, mask=mask) +import os +from os import path +import pytest +from matplotlib import pyplot + +import autocti as ac +from autofit import conf +from autocti import fixtures + + +class PlotPatch: + def __init__(self): + self.paths = [] + + def __call__(self, path, *args, **kwargs): + self.paths.append(str(path)) + + +@pytest.fixture(name="plot_patch") +def make_plot_patch(monkeypatch): + import matplotlib.figure + from unittest.mock import MagicMock + + plot_patch = PlotPatch() + monkeypatch.setattr(pyplot, "savefig", plot_patch) + monkeypatch.setattr(matplotlib.figure.Figure, "savefig", plot_patch) + monkeypatch.setattr(pyplot, "tight_layout", lambda *a, **kw: None) + monkeypatch.setattr(pyplot, "colorbar", lambda *a, **kw: MagicMock()) + return plot_patch + + +directory = path.dirname(path.realpath(__file__)) + + +@pytest.fixture(autouse=True, scope="session") +def remove_logs(): + yield + for d, _, files in os.walk(directory): + for file in files: + if file.endswith(".log"): + os.remove(path.join(d, file)) + + +@pytest.fixture(autouse=True) +def set_config_path(request): + conf.instance.push( + new_path=path.join(directory, "config"), + output_path=path.join(directory, "output"), + ) + + +### Arctic ### + + +@pytest.fixture(name="trap_0") +def make_trap_0(): + return fixtures.make_trap_0() + + +@pytest.fixture(name="trap_1") +def make_trap_1(): + return fixtures.make_trap_1() + + +@pytest.fixture(name="traps_x1") +def make_traps_x1(): + return fixtures.make_traps_x1() + + +@pytest.fixture(name="traps_x2") +def make_traps_x2(): + return fixtures.make_traps_x2() + + +@pytest.fixture(name="ccd") +def make_ccd(): + return fixtures.make_ccd() + + +@pytest.fixture(name="clocker_1d") +def make_clocker_1d(): + return fixtures.make_clocker_1d() + + +@pytest.fixture(name="parallel_clocker_2d") +def make_parallel_clocker(): + return fixtures.make_parallel_clocker_2d() + + +@pytest.fixture(name="serial_clocker_2d") +def make_serial_clocker(): + return fixtures.make_serial_clocker_2d() + + +@pytest.fixture(name="parallel_serial_clocker_2d") +def make_parallel_serial_clocker(): + return fixtures.make_parallel_serial_clocker_2d() + + +### MASK ### + + +@pytest.fixture(name="mask_1d_7_unmasked") +def make_mask_1d_7_unmasked(): + return fixtures.make_mask_1d_7_unmasked() + + +@pytest.fixture(name="mask_2d_7x7_unmasked") +def make_mask_2d_7x7_unmasked(): + return fixtures.make_mask_2d_7x7_unmasked() + + +@pytest.fixture(name="mask_2d_7x7") +def make_mask_2d_7x7(): + return fixtures.make_mask_2d_7x7() + + +### LINES ### + + +@pytest.fixture(name="layout_7") +def make_layout_7(): + return fixtures.make_layout_7() + + +@pytest.fixture(name="data_7") +def make_data_7(): + return fixtures.make_data_7() + + +@pytest.fixture(name="noise_map_7") +def make_noise_map_7(): + return fixtures.make_noise_map_7() + + +@pytest.fixture(name="pre_cti_data_7") +def make_pre_cti_data(): + return fixtures.make_pre_cti_data_7() + + +@pytest.fixture(name="dataset_1d_7") +def make_dataset_1d_7(): + return fixtures.make_dataset_1d_7() + + +@pytest.fixture(name="fit_1d_7") +def make_fit_1d_7(): + return fixtures.make_fit_1d_7() + + +### FRAMES ### + + +@pytest.fixture(name="array") +def make_array(): + return ac.Array1D.no_mask( + values=[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], pixel_scales=1.0 + ) + + +@pytest.fixture(name="image_7x7_native") +def make_image_7x7_native(): + return fixtures.make_image_7x7_native() + + +@pytest.fixture(name="noise_map_7x7_native") +def make_noise_map_7x7_native(): + return fixtures.make_noise_map_7x7_native() + + +### IMAGING ### + + +@pytest.fixture(name="imaging_7x7_frame") +def make_imaging_7x7_frame(): + return fixtures.make_imaging_7x7_frame() + + +### CHARGE INJECTION FRAMES ### + + +@pytest.fixture(name="layout_ci_7x7") +def make_layout_ci_7x7(): + return fixtures.make_layout_ci_7x7() + + +@pytest.fixture(name="ci_image_7x7") +def make_ci_image_7x7(): + return fixtures.make_ci_image_7x7() + + +@pytest.fixture(name="ci_noise_map_7x7") +def make_ci_noise_map_7x7(): + return fixtures.make_ci_noise_map_7x7() + + +@pytest.fixture(name="pre_cti_data_7x7") +def make_pre_cti_data_7x7(): + return fixtures.make_pre_cti_data_7x7() + + +@pytest.fixture(name="ci_cosmic_ray_map_7x7") +def make_ci_cosmic_ray_map_7x7(): + return fixtures.make_ci_cosmic_ray_map_7x7() + + +@pytest.fixture(name="ci_noise_scaling_map_dict_7x7") +def make_ci_noise_scaling_map_dict_7x7(): + return fixtures.make_ci_noise_scaling_map_dict_7x7() + + +### CHARGE INJECTION IMAGING ### + + +@pytest.fixture(name="imaging_ci_7x7") +def make_imaging_ci_7x7(): + return fixtures.make_imaging_ci_7x7() + + +### CHARGE INJECTION FITS ### + + +@pytest.fixture(name="hyper_noise_scalar_dict") +def make_hyper_noise_scalar_dict(): + return fixtures.make_hyper_noise_scalar_dict() + + +@pytest.fixture(name="fit_ci_7x7") +def make_fit_ci_7x7(): + return fixtures.make_fit_ci_7x7() + + +# ### PHASES ### + +from autofit.mapper.model import ModelInstance + + +@pytest.fixture(name="samples_summary_with_result") +def make_samples_summary_with_result(): + return fixtures.make_samples_summary_with_result() + + +@pytest.fixture(name="analysis_imaging_ci_7x7") +def make_analysis_imaging_ci_7x7(): + return fixtures.make_analysis_imaging_ci_7x7() + + +# Datasets + + +@pytest.fixture(name="euclid_data") +def make_euclid_data(): + return fixtures.make_euclid_data() + + +@pytest.fixture(name="acs_ccd") +def make_acs_ccd(): + return fixtures.make_acs_ccd() + + +@pytest.fixture(name="acs_quadrant") +def make_acs_quadrant(): + return fixtures.make_acs_quadrant() + + +# Custom Arrays + + +@pytest.fixture(name="parallel_array") +def make_parallel_array(): + return ac.Array2D.no_mask( + values=[ + [0.0, 0.0, 0.0], + [1.0, 1.0, 1.0], # <- Front edge . + [2.0, 2.0, 2.0], # <- Next front edge row. + [3.0, 3.0, 3.0], + [4.0, 4.0, 4.0], + [5.0, 5.0, 5.0], + [6.0, 6.0, 6.0], + [7.0, 7.0, 7.0], + [8.0, 8.0, 8.0], + [9.0, 9.0, 9.0], + ], + pixel_scales=1.0, + ) + + +@pytest.fixture(name="parallel_masked_array") +def make_parallel_masked_array(parallel_array): + mask = ac.Mask2D( + mask=[ + [False, False, False], + [False, False, False], + [False, True, False], + [False, False, True], + [False, False, False], + [False, False, False], + [False, False, False], + [True, False, False], + [False, False, False], + [False, False, False], + ], + pixel_scales=1.0, + ) + + return ac.Array2D(values=parallel_array.native, mask=mask) + + +@pytest.fixture(name="serial_array") +def make_serial_array(): + return ac.Array2D.no_mask( + values=[ + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], + ], + pixel_scales=1.0, + ) + + +@pytest.fixture(name="serial_masked_array") +def make_serial_masked_array(serial_array): + mask = ac.Mask2D( + mask=[ + [False, False, False, False, False, True, False, False, False, False], + [False, False, True, False, False, False, True, False, False, False], + [False, False, False, False, False, False, False, True, False, False], + ], + pixel_scales=1.0, + ) + + return ac.Array2D(values=serial_array.native, mask=mask) diff --git a/test_autocti/dataset_1d/model/test_plotter_dataset_1d.py b/test_autocti/dataset_1d/model/test_plotter_dataset_1d.py new file mode 100644 index 00000000..73e6dbda --- /dev/null +++ b/test_autocti/dataset_1d/model/test_plotter_dataset_1d.py @@ -0,0 +1,97 @@ +import shutil +from pathlib import Path + +import pytest + +from autocti.dataset_1d.model.plotter import PlotterDataset1D + +directory = Path(__file__).resolve().parent + + +@pytest.fixture(name="plot_path") +def make_plot_path(): + return directory / "files" + + +def test__dataset(dataset_1d_7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterDataset1D(image_path=plot_path) + + plotter.dataset(dataset=dataset_1d_7) + plotter.dataset_regions(dataset=dataset_1d_7, region_list=["fpr", "eper"]) + + assert str(Path(plot_path) / "dataset" / "subplot_dataset.png") in plot_patch.paths + assert ( + str(Path(plot_path) / "dataset" / "subplot_dataset_fpr.png") in plot_patch.paths + ) + assert str(Path(plot_path) / "dataset" / "data_eper.png") in plot_patch.paths + + +def test__dataset_combined(dataset_1d_7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterDataset1D(image_path=plot_path) + + plotter.dataset_combined(dataset_list=[dataset_1d_7, dataset_1d_7]) + plotter.dataset_regions_combined( + dataset_list=[dataset_1d_7, dataset_1d_7], region_list=["fpr"] + ) + + assert ( + str(Path(plot_path) / "dataset" / "subplot_data_list.png") in plot_patch.paths + ) + assert ( + str(Path(plot_path) / "dataset" / "subplot_data_list_fpr.png") + in plot_patch.paths + ) + + +def test__fit(fit_1d_7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterDataset1D(image_path=plot_path) + + plotter.fit(fit=fit_1d_7, during_analysis=True) + plotter.fit_regions(fit=fit_1d_7, region_list=["fpr", "eper"], during_analysis=True) + + assert str(Path(plot_path) / "fit_dataset" / "subplot_fit.png") in plot_patch.paths + assert ( + str(Path(plot_path) / "fit_dataset" / "subplot_fit_eper.png") + in plot_patch.paths + ) + + +def test__fit__not_during_analysis_outputs_fits(fit_1d_7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterDataset1D(image_path=plot_path) + + plotter.fit(fit=fit_1d_7, during_analysis=False) + + assert (Path(plot_path) / "fit_dataset" / "fit.fits").exists() + + +def test__fit_combined(fit_1d_7, plot_path, plot_patch): + if Path(plot_path).exists(): + shutil.rmtree(plot_path) + + plotter = PlotterDataset1D(image_path=plot_path) + + plotter.fit_combined(fit_list=[fit_1d_7, fit_1d_7], during_analysis=True) + plotter.fit_region_combined( + fit_list=[fit_1d_7, fit_1d_7], region_list=["eper"], during_analysis=True + ) + + assert ( + str(Path(plot_path) / "fit_dataset_combined" / "subplot_data_list.png") + in plot_patch.paths + ) + assert ( + str(Path(plot_path) / "fit_dataset_combined" / "subplot_data_list_eper.png") + in plot_patch.paths + ) diff --git a/test_autocti/dataset_1d/model/test_plotter_interface_1d.py b/test_autocti/dataset_1d/model/test_plotter_interface_1d.py deleted file mode 100644 index a2767ef9..00000000 --- a/test_autocti/dataset_1d/model/test_plotter_interface_1d.py +++ /dev/null @@ -1,113 +0,0 @@ -import os -import shutil -from os import path - -import pytest -from autocti.dataset_1d.model.plotter_interface import PlotterInterfaceDataset1D - -directory = path.dirname(path.abspath(__file__)) - - -@pytest.fixture(name="plot_path") -def make_visualizer_plotter_setup(): - return path.join("{}".format(directory), "files") - - -def test__dataset_1d(dataset_1d_7, plot_path, plot_patch): - if path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceDataset1D(image_path=plot_path) - - visualizer.dataset(dataset=dataset_1d_7) - - plot_path = path.join(plot_path, "dataset") - - assert path.join(plot_path, "subplot_dataset.png") in plot_patch.paths - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - - -def test__dataset_1d_region(dataset_1d_7, plot_path, plot_patch): - if path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceDataset1D(image_path=plot_path) - - visualizer.dataset_regions(dataset=dataset_1d_7, region_list=["fpr"]) - - plot_path = path.join(plot_path, "dataset") - - assert path.join(plot_path, "subplot_dataset_fpr.png") in plot_patch.paths - assert path.join(plot_path, "data_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_fpr.png") not in plot_patch.paths - - -def test__fit_1d(fit_1d_7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceDataset1D(image_path=plot_path) - - visualizer.fit(fit=fit_1d_7, during_analysis=True) - - plot_path = path.join(plot_path, "fit_dataset") - - assert path.join(plot_path, "subplot_fit.png") in plot_patch.paths - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - - plot_patch.paths = [] - - visualizer.fit(fit=fit_1d_7, during_analysis=False) - - assert path.join(plot_path, "subplot_fit.png") in plot_patch.paths - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") in plot_patch.paths - - -def test__fit_1d_region(fit_1d_7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceDataset1D(image_path=plot_path) - - visualizer.fit_regions(fit=fit_1d_7, region_list=["fpr"], during_analysis=True) - - plot_path = path.join(plot_path, "fit_dataset") - - assert path.join(plot_path, "subplot_fit_fpr.png") in plot_patch.paths - assert path.join(plot_path, "data_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_fpr.png") not in plot_patch.paths - - -def test__fit_combined(fit_1d_7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceDataset1D(image_path=plot_path) - - visualizer.fit_combined(fit_list=[fit_1d_7, fit_1d_7], during_analysis=True) - - plot_path = path.join(plot_path, "fit_dataset_combined") - - assert path.join(plot_path, "subplot_residual_map.png") in plot_patch.paths - assert path.join(plot_path, "subplot_chi_squared_map.png") not in plot_patch.paths - - -def test__fit_region(fit_1d_7, plot_path, plot_patch): - if os.path.exists(plot_path): - shutil.rmtree(plot_path) - - visualizer = PlotterInterfaceDataset1D(image_path=plot_path) - - visualizer.fit_region_combined( - fit_list=[fit_1d_7, fit_1d_7], region_list=["fpr"], during_analysis=True - ) - - plot_path = path.join(plot_path, "fit_dataset_combined") - - assert path.join(plot_path, "subplot_residual_map_fpr.png") in plot_patch.paths - assert ( - path.join(plot_path, "subplot_chi_squared_map_fpr.png") not in plot_patch.paths - ) diff --git a/test_autocti/dataset_1d/plot/__init__.py b/test_autocti/dataset_1d/plot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test_autocti/dataset_1d/plot/test_dataset_1d_plots.py b/test_autocti/dataset_1d/plot/test_dataset_1d_plots.py new file mode 100644 index 00000000..12e5cdcc --- /dev/null +++ b/test_autocti/dataset_1d/plot/test_dataset_1d_plots.py @@ -0,0 +1,61 @@ +from pathlib import Path + +import pytest + +import autocti.plot as aplt + +directory = Path(__file__).resolve().parent + + +@pytest.fixture(name="plot_path") +def make_plot_path(): + return directory / "files" / "plots" + + +def test__figure_data(dataset_1d_7, plot_path, plot_patch): + aplt.figure_dataset_1d_data( + dataset=dataset_1d_7, output_path=plot_path, output_format="png" + ) + + assert str(Path(plot_path) / "data.png") in plot_patch.paths + + +def test__figure_data__region_and_logy(dataset_1d_7, plot_path, plot_patch): + aplt.figure_dataset_1d_data( + dataset=dataset_1d_7, + region="fpr", + output_path=plot_path, + output_format="png", + ) + aplt.figure_dataset_1d_data( + dataset=dataset_1d_7, + region="eper", + logy=True, + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "data_fpr.png") in plot_patch.paths + assert str(Path(plot_path) / "data_logy_eper.png") in plot_patch.paths + + +def test__subplot_dataset(dataset_1d_7, plot_path, plot_patch): + aplt.subplot_dataset_1d( + dataset=dataset_1d_7, output_path=plot_path, output_format="png" + ) + aplt.subplot_dataset_1d( + dataset=dataset_1d_7, region="fpr", output_path=plot_path, output_format="png" + ) + + assert str(Path(plot_path) / "subplot_dataset.png") in plot_patch.paths + assert str(Path(plot_path) / "subplot_dataset_fpr.png") in plot_patch.paths + + +def test__subplot_dataset_list(dataset_1d_7, plot_path, plot_patch): + aplt.subplot_dataset_1d_list( + dataset_list=[dataset_1d_7, dataset_1d_7], + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "subplot_data_list.png") in plot_patch.paths diff --git a/test_autocti/dataset_1d/plot/test_dataset_1d_plotters.py b/test_autocti/dataset_1d/plot/test_dataset_1d_plotters.py deleted file mode 100644 index 61c547ca..00000000 --- a/test_autocti/dataset_1d/plot/test_dataset_1d_plotters.py +++ /dev/null @@ -1,84 +0,0 @@ -from os import path - -import pytest - -from autocti import plot as aplt - -directory = path.dirname(path.realpath(__file__)) - - -@pytest.fixture(name="plot_path") -def make_dataset_plotter_setup(): - return path.join( - "{}".format(path.dirname(path.realpath(__file__))), - "files", - "plots", - "dataset_1d", - ) - - -def test__individual_attributes_are_output(dataset_1d_7, plot_path, plot_patch): - dataset_plotter = aplt.Dataset1DPlotter( - dataset=dataset_1d_7, - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - dataset_plotter.figures_1d( - data=True, noise_map=True, pre_cti_data=True, signal_to_noise_map=True - ) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") in plot_patch.paths - - plot_patch.paths = [] - - dataset_plotter.figures_1d(data=True, pre_cti_data=True) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") not in plot_patch.paths - - -def test__individual_1d_of_region_are_output(dataset_1d_7, plot_path, plot_patch): - dataset_plotter = aplt.Dataset1DPlotter( - dataset=dataset_1d_7, - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - dataset_plotter.figures_1d( - region="fpr", - data=True, - noise_map=True, - pre_cti_data=True, - signal_to_noise_map=True, - ) - - assert path.join(plot_path, "data_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_fpr.png") in plot_patch.paths - assert path.join(plot_path, "pre_cti_data_fpr.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map_fpr.png") in plot_patch.paths - - plot_patch.paths = [] - - dataset_plotter.figures_1d(region="fpr", data=True, pre_cti_data=True) - - assert path.join(plot_path, "data_fpr.png") in plot_patch.paths - assert path.join(plot_path, "noise_map_fpr.png") not in plot_patch.paths - assert path.join(plot_path, "pre_cti_data_fpr.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map_fpr.png") not in plot_patch.paths - - -def test__subplots__output(dataset_1d_7, plot_path, plot_patch): - dataset_plotter = aplt.Dataset1DPlotter( - dataset=dataset_1d_7, - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - dataset_plotter.subplot_dataset() - assert path.join(plot_path, "subplot_dataset.png") in plot_patch.paths - - dataset_plotter.subplot_dataset(region="fpr") - assert path.join(plot_path, "subplot_dataset_fpr.png") in plot_patch.paths diff --git a/test_autocti/dataset_1d/plot/test_fit_plots.py b/test_autocti/dataset_1d/plot/test_fit_plots.py new file mode 100644 index 00000000..e647783f --- /dev/null +++ b/test_autocti/dataset_1d/plot/test_fit_plots.py @@ -0,0 +1,65 @@ +from pathlib import Path + +import pytest + +import autocti.plot as aplt + +directory = Path(__file__).resolve().parent + + +@pytest.fixture(name="plot_path") +def make_plot_path(): + return directory / "files" / "plots" + + +def test__figure_fit(fit_1d_7, plot_path, plot_patch): + aplt.figure_fit_dataset_1d( + fit=fit_1d_7, quantity="data", output_path=plot_path, output_format="png" + ) + aplt.figure_fit_dataset_1d( + fit=fit_1d_7, + quantity="residual_map", + region="eper", + logy=True, + output_path=plot_path, + output_format="png", + ) + aplt.figure_fit_dataset_1d( + fit=fit_1d_7, + quantity="chi_squared_map", + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "data.png") in plot_patch.paths + assert str(Path(plot_path) / "residual_map_logy_eper.png") in plot_patch.paths + assert str(Path(plot_path) / "chi_squared_map.png") in plot_patch.paths + + +def test__subplot_fit(fit_1d_7, plot_path, plot_patch): + aplt.subplot_fit_dataset_1d( + fit=fit_1d_7, output_path=plot_path, output_format="png" + ) + aplt.subplot_fit_dataset_1d( + fit=fit_1d_7, region="fpr", output_path=plot_path, output_format="png" + ) + + assert str(Path(plot_path) / "subplot_fit.png") in plot_patch.paths + assert str(Path(plot_path) / "subplot_fit_fpr.png") in plot_patch.paths + + +def test__subplot_fit_list(fit_1d_7, plot_path, plot_patch): + aplt.subplot_fit_dataset_1d_list( + fit_list=[fit_1d_7, fit_1d_7], + quantity="residual_map", + output_path=plot_path, + output_format="png", + ) + + assert str(Path(plot_path) / "subplot_residual_map_list.png") in plot_patch.paths + + +def test__fits_fit(fit_1d_7, plot_path, tmp_path): + aplt.fits_fit_dataset_1d(fit=fit_1d_7, output_path=tmp_path) + + assert (Path(tmp_path) / "fit.fits").exists() diff --git a/test_autocti/dataset_1d/plot/test_fit_plotters.py b/test_autocti/dataset_1d/plot/test_fit_plotters.py deleted file mode 100644 index d043b021..00000000 --- a/test_autocti/dataset_1d/plot/test_fit_plotters.py +++ /dev/null @@ -1,70 +0,0 @@ -from os import path -import pytest -from autocti import plot as aplt - -directory = path.dirname(path.realpath(__file__)) - - -@pytest.fixture(name="plot_path") -def make_fit_plotter_setup(): - return path.join( - "{}".format(path.dirname(path.realpath(__file__))), "files", "plots", "fit" - ) - - -def test__individual_attribute_plots__all_plot_correctly( - fit_1d_7, plot_path, plot_patch -): - fit_plotter = aplt.FitDataset1DPlotter( - fit=fit_1d_7, - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - fit_plotter.figures_1d( - data=True, - noise_map=True, - signal_to_noise_map=True, - pre_cti_data=True, - post_cti_data=True, - residual_map=True, - normalized_residual_map=True, - chi_squared_map=True, - ) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "post_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "residual_map.png") in plot_patch.paths - assert path.join(plot_path, "normalized_residual_map.png") in plot_patch.paths - assert path.join(plot_path, "chi_squared_map.png") in plot_patch.paths - - plot_patch.paths = [] - - fit_plotter.figures_1d( - data=True, - noise_map=False, - signal_to_noise_map=False, - pre_cti_data=True, - post_cti_data=True, - chi_squared_map=True, - ) - - assert path.join(plot_path, "data.png") in plot_patch.paths - assert path.join(plot_path, "noise_map.png") not in plot_patch.paths - assert path.join(plot_path, "signal_to_noise_map.png") not in plot_patch.paths - assert path.join(plot_path, "pre_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "post_cti_data.png") in plot_patch.paths - assert path.join(plot_path, "residual_map.png") not in plot_patch.paths - assert path.join(plot_path, "chi_squared_map.png") in plot_patch.paths - - -def test__fit_1d_subplots_are_output(fit_1d_7, plot_path, plot_patch): - fit_plotter = aplt.FitDataset1DPlotter( - fit=fit_1d_7, - mat_plot_1d=aplt.MatPlot1D(output=aplt.Output(plot_path, format="png")), - ) - - fit_plotter.subplot_fit() - assert path.join(plot_path, "subplot_fit.png") in plot_patch.paths diff --git a/test_autocti/plot/test_abstract_plotters.py b/test_autocti/plot/test_abstract_plotters.py deleted file mode 100644 index c6828034..00000000 --- a/test_autocti/plot/test_abstract_plotters.py +++ /dev/null @@ -1,30 +0,0 @@ -import copy -from os import path -import pytest -from autocti import plot as aplt - -directory = path.dirname(path.realpath(__file__)) - - -@pytest.fixture(name="plot_path") -def make_fit_plotter_setup(): - return path.join( - "{}".format(path.dirname(path.realpath(__file__))), "files", "plots", "fit" - ) - - -def test__text_manual_dict(fit_ci_7x7): - fit_ci_7x7 = copy.deepcopy(fit_ci_7x7) - fit_ci_7x7.dataset.settings_dict = {"hello": 2.0, "hi": 3.0} - - fit_plotter = aplt.FitImagingCIPlotter(fit=fit_ci_7x7) - - assert fit_plotter.text_manual_dict_from(region="eper") == { - "FPR (e-)": 1.0, - "hello": 2.0, - "hi": 3.0, - } - assert fit_plotter.text_manual_dict_from(region="fpr") == { - "hello": 2.0, - "hi": 3.0, - }