From 22fc982e2088d408b149a1726ea20aa7627e80d3 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Thu, 20 Feb 2025 16:18:59 +0000 Subject: [PATCH 1/4] shipwreck of a multi fits outputter works --- autoarray/config/visualize/plots.yaml | 29 +---- .../inversion/inversion/mapper_valued.py | 2 +- autoarray/operators/transformer.py | 2 +- autoarray/plot/multi_plotters.py | 105 ++++++++++++++---- autoarray/plot/wrap/base/output.py | 14 ++- autoarray/plot/wrap/two_d/voronoi_drawer.py | 2 +- autoarray/structures/arrays/array_2d_util.py | 32 ++++++ 7 files changed, 131 insertions(+), 55 deletions(-) diff --git a/autoarray/config/visualize/plots.yaml b/autoarray/config/visualize/plots.yaml index da0e330ef..aa2664506 100644 --- a/autoarray/config/visualize/plots.yaml +++ b/autoarray/config/visualize/plots.yaml @@ -5,19 +5,11 @@ dataset: # Settings for plots of all datasets (e.g. ImagingPlotter, InterferometerPlotter). subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the data, noise-map, etc.)? - data: false # Plot the individual data of every dataset? - noise_map: false # Plot the individual noise-map of every dataset? - signal_to_noise_map: false # Plot the individual signal-to-noise-map of every dataset? - over_sample_size_lp: false # Plot the over-sampling size, used to evaluate light profiles, of every dataset? - over_sample_size_pixelization: false # Plot the over-sampling size, used to evaluate pixelizations, of every dataset? imaging: # Settings for plots of imaging datasets (e.g. ImagingPlotter) psf: false fit: # Settings for plots of all fits (e.g. FitImagingPlotter, FitInterferometerPlotter). subplot_fit: true # Plot subplot of all fit quantities for any dataset (e.g. the model data, residual-map, etc.)? subplot_fit_log10: true # Plot subplot of all fit quantities for any dataset using log10 color maps (e.g. the model data, residual-map, etc.)? - all_at_end_png: true # Plot all individual plots listed below as .png (even if False)? - all_at_end_fits: true # Plot all individual plots listed below as .fits (even if False)? - all_at_end_pdf: false # Plot all individual plots listed below as publication-quality .pdf (even if False)? data: false # Plot individual plots of the data? noise_map: false # Plot individual plots of the noise-map? signal_to_noise_map: false # Plot individual plots of the signal-to-noise-map? @@ -30,9 +22,6 @@ fit_imaging: {} # Settings for plots of fits to imagi inversion: # Settings for plots of inversions (e.g. InversionPlotter). subplot_inversion: true # Plot subplot of all quantities in each inversion (e.g. reconstrucuted image, reconstruction)? subplot_mappings: true # Plot subplot of the image-to-source pixels mappings of each pixelization? - all_at_end_png: true # Plot all individual plots listed below as .png (even if False)? - all_at_end_fits: true # Plot all individual plots listed below as .fits (even if False)? - all_at_end_pdf: false # Plot all individual plots listed below as publication-quality .pdf (even if False)? data_subtracted: false # Plot individual plots of the data with the other inversion linear objects subtracted? reconstruction_noise_map: false # Plot image of the noise of every mesh-pixel reconstructed value? sub_pixels_per_image_pixels: false # Plot the number of sub pixels per masked data pixels? @@ -41,22 +30,6 @@ inversion: # Settings for plots of inversions (e reconstructed_image: false # Plot image of the reconstructed data (e.g. in the image-plane)? reconstruction: false # Plot the reconstructed inversion (e.g. the pixelization's mesh in the source-plane)? regularization_weights: false # Plot the effective regularization weight of every inversion mesh pixel? -interferometer: # Settings for plots of interferometer datasets (e.g. InterferometerPlotter). - amplitudes_vs_uv_distances: false - phases_vs_uv_distances: false - uv_wavelengths: false - dirty_image: false - dirty_noise_map: false - dirty_signal_to_noise_map: false fit_interferometer: # Settings for plots of fits to interferometer datasets (e.g. FitInterferometerPlotter). subplot_fit_dirty_images: false # Plot subplot of the dirty-images of all interferometer datasets? - subplot_fit_real_space: false # Plot subplot of the real-space images of all interferometer datasets? - amplitudes_vs_uv_distances: false - phases_vs_uv_distances: false - uv_wavelengths: false - dirty_image: false - dirty_noise_map: false - dirty_signal_to_noise_map: false - dirty_residual_map: false - dirty_normalized_residual_map: false - dirty_chi_squared_map: false \ No newline at end of file + subplot_fit_real_space: false # Plot subplot of the real-space images of all interferometer datasets? \ No newline at end of file diff --git a/autoarray/inversion/inversion/mapper_valued.py b/autoarray/inversion/inversion/mapper_valued.py index f12bec261..175046b9d 100644 --- a/autoarray/inversion/inversion/mapper_valued.py +++ b/autoarray/inversion/inversion/mapper_valued.py @@ -259,7 +259,7 @@ def magnification_via_mesh_from( def magnification_via_interpolation_from( self, - shape_native: Tuple[int, int] = (401, 401), + shape_native: Tuple[int, int] = (201, 201), extent: Optional[Tuple[float, float, float, float]] = None, ) -> float: """ diff --git a/autoarray/operators/transformer.py b/autoarray/operators/transformer.py index 89e48ecf2..acbe6bb73 100644 --- a/autoarray/operators/transformer.py +++ b/autoarray/operators/transformer.py @@ -47,7 +47,7 @@ def pylops_exception(): "\n--------------------\n" "You are attempting to perform interferometer analysis.\n\n" "However, the optional library PyLops (https://github.com/PyLops/pylops) is not installed.\n\n" - "Install it via the command `pip install pylops==1.18.3`.\n\n" + "Install it via the command `pip install pylops==2.3.1`.\n\n" "----------------------" ) diff --git a/autoarray/plot/multi_plotters.py b/autoarray/plot/multi_plotters.py index 01ed8364f..92227aa76 100644 --- a/autoarray/plot/multi_plotters.py +++ b/autoarray/plot/multi_plotters.py @@ -1,5 +1,10 @@ +from astropy.io import fits +import numpy as np +import os from typing import List, Optional, Tuple +from autoconf import conf + from autoarray.plot.wrap.base.ticks import YTicks from autoarray.plot.wrap.base.ticks import XTicks @@ -104,29 +109,6 @@ def plot_via_func(self, plotter, figure_name: str, func_name: str, kwargs): else: func(**{**{figure_name: True}, **kwargs}) - def output_subplot(self, filename_suffix: str = ""): - """ - Outplot the subplot to a file after all figures have been plotted on the subplot. - - The multi-plotter requires its own output function to ensure that the subplot is output to a file, which - this provides. - - Parameters - ---------- - filename_suffix - The suffix of the filename that the subplot is output to. - """ - - if self.plotter_list[0].mat_plot_1d is not None: - self.plotter_list[0].mat_plot_1d.output.subplot_to_figure( - auto_filename=f"subplot_{filename_suffix}" - ) - if self.plotter_list[0].mat_plot_2d is not None: - self.plotter_list[0].mat_plot_2d.output.subplot_to_figure( - auto_filename=f"subplot_{filename_suffix}" - ) - self.plotter_list[0].close_subplot_figure() - def subplot_of_figure( self, func_name: str, figure_name: str, filename_suffix: str = "", **kwargs ): @@ -266,6 +248,83 @@ def subplot_of_multi_yx_1d(self, filename_suffix="", **kwargs): ) self.plotter_list[0].plotter_list[0].close_subplot_figure() + def output_subplot(self, filename_suffix: str = ""): + """ + Outplot the subplot to a file after all figures have been plotted on the subplot. + + The multi-plotter requires its own output function to ensure that the subplot is output to a file, which + this provides. + + Parameters + ---------- + filename_suffix + The suffix of the filename that the subplot is output to. + """ + + if self.plotter_list[0].mat_plot_1d is not None: + self.plotter_list[0].mat_plot_1d.output.subplot_to_figure( + auto_filename=f"subplot_{filename_suffix}" + ) + if self.plotter_list[0].mat_plot_2d is not None: + self.plotter_list[0].mat_plot_2d.output.subplot_to_figure( + auto_filename=f"subplot_{filename_suffix}" + ) + self.plotter_list[0].close_subplot_figure() + + def output_to_fits( + self, + func_name_list: List[str], + figure_name_list: List[str], + filename: str, + tag_list: Optional[List[str]] = None, + remove_fits_first : bool = False, + **kwargs, + ): + """ + Outputs a subplot of figures of the plotter objects in the `plotter_list`, where multiple function names and + figure names are input. + + For example, if you have multiple `ImagingPlotter` objects and you want to plot the `data` and `noise_map` of + each on the same subplot, you would input `func_name_list=['figures_2d', 'figures_2d']` and + `figure_name_list=['data', 'noise_map']`. + + Parameters + ---------- + func_name_list + The list of function names that are called to plot the figures on the subplot. + figure_name_list + The list of figure names that are plotted on the subplot. + filename_suffix + The suffix of the filename that the subplot is output to. + kwargs + Any additional keyword arguments that are passed to the function that plots the figure on the subplot. + """ + + output_path = self.plotter_list[0].mat_plot_2d.output.output_path_from(format="fits_multi") + output_fits_file = os.path.join(output_path, f"{filename}.fits") + + if remove_fits_first and os.path.exists(output_fits_file): + os.remove(output_fits_file) + + for i, plotter in enumerate(self.plotter_list): + + plotter.mat_plot_2d.output._format = "fits_multi" + + plotter.set_filename(filename=f"{filename}") + + for j, (func_name, figure_name) in enumerate( + zip(func_name_list, figure_name_list) + ): + + if tag_list is not None: + plotter.mat_plot_2d.output._tag_fits_multi = tag_list[j] + + self.plot_via_func( + plotter=plotter, + figure_name=figure_name, + func_name=func_name, + kwargs=kwargs, + ) class MultiYX1DPlotter: def __init__( diff --git a/autoarray/plot/wrap/base/output.py b/autoarray/plot/wrap/base/output.py index 6646e8c21..063c8be1a 100644 --- a/autoarray/plot/wrap/base/output.py +++ b/autoarray/plot/wrap/base/output.py @@ -61,6 +61,7 @@ def __init__( self.format_folder = format_folder self.bypass = bypass self.bbox_inches = bbox_inches + self._tag_fits_multi = None self.kwargs = kwargs @@ -104,7 +105,7 @@ def savefig(self, filename: str, output_path: str, format: str): try: plt.savefig( path.join(output_path, f"{filename}.{format}"), - bbox_inches=self.bbox_inches, + bbox_inches=self.bbox_inches, pad_inches=0 ) except ValueError as e: logger.info( @@ -151,6 +152,17 @@ def to_figure( file_path=path.join(output_path, f"{filename}.fits"), overwrite=True, ) + elif format == "fits_multi": + if structure is not None: + + from autoarray.structures.arrays.array_2d_util import update_fits_file + + update_fits_file( + arr=structure.native, + file_path=path.join(output_path, f"{filename}.fits"), + tag=self._tag_fits_multi + ) + def subplot_to_figure(self, auto_filename: Optional[str] = None): """ diff --git a/autoarray/plot/wrap/two_d/voronoi_drawer.py b/autoarray/plot/wrap/two_d/voronoi_drawer.py index 9e189413b..aec6661cc 100644 --- a/autoarray/plot/wrap/two_d/voronoi_drawer.py +++ b/autoarray/plot/wrap/two_d/voronoi_drawer.py @@ -81,7 +81,7 @@ def draw_voronoi_pixels( cmap = plt.get_cmap(cmap.cmap) - if colorbar is not None: + if colorbar is not None and colorbar is not False: cb = colorbar.set_with_color_values( units=units, norm=norm, diff --git a/autoarray/structures/arrays/array_2d_util.py b/autoarray/structures/arrays/array_2d_util.py index 72cb8437e..655e27844 100644 --- a/autoarray/structures/arrays/array_2d_util.py +++ b/autoarray/structures/arrays/array_2d_util.py @@ -862,3 +862,35 @@ def header_obj_from(file_path: Union[Path, str], hdu: int) -> Dict: hdu_list = fits.open(file_path) return hdu_list[hdu].header + + +def update_fits_file(arr, file_path, tag=None, header=None): + + if header is None: + header = fits.Header() + + try: + header["HELLO"] = "GII" + header["PIXSCAY"] = str(arr.pixel_scales[0]) + header["PIXSCAX"] = str(arr.pixel_scales[1]) + except AttributeError: + pass + + if conf.instance["general"]["fits"]["flip_for_ds9"]: + arr = np.flipud(arr) + + if os.path.exists(file_path): + + with fits.open(file_path, mode='update') as hdul: + hdul.append(fits.ImageHDU(arr, header)) + if tag is not None: + hdul[-1].header['EXTNAME'] = tag.upper() + hdul.flush() + + else: + + hdu = fits.PrimaryHDU(arr, header) + if tag is not None: + hdu.header['EXTNAME'] = tag.upper() + hdul = fits.HDUList([hdu]) + hdul.writeto(file_path, overwrite=True) \ No newline at end of file From f850a522ae4cbc3353423344f6be85d64fcd6901 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Thu, 20 Feb 2025 18:47:45 +0000 Subject: [PATCH 2/4] black --- autoarray/plot/multi_plotters.py | 9 +++++---- autoarray/plot/wrap/base/output.py | 11 ++++++----- autoarray/structures/arrays/array_2d_util.py | 11 ++++------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/autoarray/plot/multi_plotters.py b/autoarray/plot/multi_plotters.py index 92227aa76..d4f64bbf6 100644 --- a/autoarray/plot/multi_plotters.py +++ b/autoarray/plot/multi_plotters.py @@ -277,7 +277,7 @@ def output_to_fits( figure_name_list: List[str], filename: str, tag_list: Optional[List[str]] = None, - remove_fits_first : bool = False, + remove_fits_first: bool = False, **kwargs, ): """ @@ -300,14 +300,15 @@ def output_to_fits( Any additional keyword arguments that are passed to the function that plots the figure on the subplot. """ - output_path = self.plotter_list[0].mat_plot_2d.output.output_path_from(format="fits_multi") + output_path = self.plotter_list[0].mat_plot_2d.output.output_path_from( + format="fits_multi" + ) output_fits_file = os.path.join(output_path, f"{filename}.fits") if remove_fits_first and os.path.exists(output_fits_file): os.remove(output_fits_file) for i, plotter in enumerate(self.plotter_list): - plotter.mat_plot_2d.output._format = "fits_multi" plotter.set_filename(filename=f"{filename}") @@ -315,7 +316,6 @@ def output_to_fits( for j, (func_name, figure_name) in enumerate( zip(func_name_list, figure_name_list) ): - if tag_list is not None: plotter.mat_plot_2d.output._tag_fits_multi = tag_list[j] @@ -326,6 +326,7 @@ def output_to_fits( kwargs=kwargs, ) + class MultiYX1DPlotter: def __init__( self, diff --git a/autoarray/plot/wrap/base/output.py b/autoarray/plot/wrap/base/output.py index 063c8be1a..945f91abd 100644 --- a/autoarray/plot/wrap/base/output.py +++ b/autoarray/plot/wrap/base/output.py @@ -105,7 +105,8 @@ def savefig(self, filename: str, output_path: str, format: str): try: plt.savefig( path.join(output_path, f"{filename}.{format}"), - bbox_inches=self.bbox_inches, pad_inches=0 + bbox_inches=self.bbox_inches, + pad_inches=0, ) except ValueError as e: logger.info( @@ -154,16 +155,16 @@ def to_figure( ) elif format == "fits_multi": if structure is not None: - - from autoarray.structures.arrays.array_2d_util import update_fits_file + from autoarray.structures.arrays.array_2d_util import ( + update_fits_file, + ) update_fits_file( arr=structure.native, file_path=path.join(output_path, f"{filename}.fits"), - tag=self._tag_fits_multi + tag=self._tag_fits_multi, ) - def subplot_to_figure(self, auto_filename: Optional[str] = None): """ Output a subplot figure, either as an image on the screen or to the hard-disk as a png or fits file. diff --git a/autoarray/structures/arrays/array_2d_util.py b/autoarray/structures/arrays/array_2d_util.py index 655e27844..2ebe6cdfb 100644 --- a/autoarray/structures/arrays/array_2d_util.py +++ b/autoarray/structures/arrays/array_2d_util.py @@ -865,7 +865,6 @@ def header_obj_from(file_path: Union[Path, str], hdu: int) -> Dict: def update_fits_file(arr, file_path, tag=None, header=None): - if header is None: header = fits.Header() @@ -880,17 +879,15 @@ def update_fits_file(arr, file_path, tag=None, header=None): arr = np.flipud(arr) if os.path.exists(file_path): - - with fits.open(file_path, mode='update') as hdul: + with fits.open(file_path, mode="update") as hdul: hdul.append(fits.ImageHDU(arr, header)) if tag is not None: - hdul[-1].header['EXTNAME'] = tag.upper() + hdul[-1].header["EXTNAME"] = tag.upper() hdul.flush() else: - hdu = fits.PrimaryHDU(arr, header) if tag is not None: - hdu.header['EXTNAME'] = tag.upper() + hdu.header["EXTNAME"] = tag.upper() hdul = fits.HDUList([hdu]) - hdul.writeto(file_path, overwrite=True) \ No newline at end of file + hdul.writeto(file_path, overwrite=True) From b5c80d0563036432332c981b840863bb90785955 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Fri, 21 Feb 2025 10:04:20 +0000 Subject: [PATCH 3/4] docstrings --- autoarray/plot/multi_plotters.py | 27 ++++++++++++----- autoarray/structures/arrays/array_2d_util.py | 32 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/autoarray/plot/multi_plotters.py b/autoarray/plot/multi_plotters.py index d4f64bbf6..66d00d864 100644 --- a/autoarray/plot/multi_plotters.py +++ b/autoarray/plot/multi_plotters.py @@ -281,12 +281,20 @@ def output_to_fits( **kwargs, ): """ - Outputs a subplot of figures of the plotter objects in the `plotter_list`, where multiple function names and - figure names are input. + Outputs a list of figures of the plotter objects in the `plotter_list` to a single .fits file. - For example, if you have multiple `ImagingPlotter` objects and you want to plot the `data` and `noise_map` of - each on the same subplot, you would input `func_name_list=['figures_2d', 'figures_2d']` and - `figure_name_list=['data', 'noise_map']`. + This function takes as input lists of function names and figure names and then calls them via + the `plotter_list` with an interface that outputs each to a .fits file. + + For example, if you have multiple `ImagingPlotter` objects and want to output the `data` and `noise_map` of + each to a single .fits files, you would input: + + - `func_name_list=['figures_2d', 'figures_2d']` and + - `figure_name_list=['data', 'noise_map']`. + + The implementation of this code is hacky, with it using a specific interface in the `Output` object + which sets the format to `fits_multi` to call a function which outputs the .fits files. A major visualuzation + refactor is required to make this more elegant. Parameters ---------- @@ -294,8 +302,13 @@ def output_to_fits( The list of function names that are called to plot the figures on the subplot. figure_name_list The list of figure names that are plotted on the subplot. - filename_suffix - The suffix of the filename that the subplot is output to. + filenane + The filename that the .fits file is output to. + tag_list + The list of tags that are used to set the `EXTNAME` of each hdu of the .fits file. + remove_fits_first + If the .fits file already exists, it is removed before the new .fits file is output, else it is updated + with the figure going into the next hdu. kwargs Any additional keyword arguments that are passed to the function that plots the figure on the subplot. """ diff --git a/autoarray/structures/arrays/array_2d_util.py b/autoarray/structures/arrays/array_2d_util.py index 2ebe6cdfb..601cd9f48 100644 --- a/autoarray/structures/arrays/array_2d_util.py +++ b/autoarray/structures/arrays/array_2d_util.py @@ -864,12 +864,40 @@ def header_obj_from(file_path: Union[Path, str], hdu: int) -> Dict: return hdu_list[hdu].header -def update_fits_file(arr, file_path, tag=None, header=None): +def update_fits_file( + arr: np.ndarray, + file_path: str, + tag: Optional[str] = None, + header: Optional[fits.Header] = None, +): + """ + Update a .fits file with a new array. + + This function is used by the `fits_multi` output interface so that a single .fits file with groups of data + in hdu's can be created. + + It may receive a `tag` which is used to set the `EXTNAME` of the HDU in the .fits file and therefore is the name + of the hdu seen by the user when they open it with DS9 or other .fits software. + + A header may also be provided, which by default has the pixel scales of the array added to it. + + Parameters + ---------- + arr + The array that is written to the .fits file. + file_path + The full path of the file that is output, including the file name and ``.fits`` extension. + tag + The `EXTNAME` of the HDU in the .fits file. + header + The header of the .fits file that the array is written to, which if blank will still contain the pixel scales + of the array. + """ + if header is None: header = fits.Header() try: - header["HELLO"] = "GII" header["PIXSCAY"] = str(arr.pixel_scales[0]) header["PIXSCAX"] = str(arr.pixel_scales[1]) except AttributeError: From b8a639735786a08da8491ddccfa5d98f139c1a8a Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Fri, 21 Feb 2025 15:16:51 +0000 Subject: [PATCH 4/4] review --- autoarray/plot/multi_plotters.py | 23 ++++++++++---------- autoarray/structures/arrays/array_2d_util.py | 5 +++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/autoarray/plot/multi_plotters.py b/autoarray/plot/multi_plotters.py index 66d00d864..5c2c5071e 100644 --- a/autoarray/plot/multi_plotters.py +++ b/autoarray/plot/multi_plotters.py @@ -1,10 +1,7 @@ -from astropy.io import fits -import numpy as np import os +from pathlib import Path from typing import List, Optional, Tuple -from autoconf import conf - from autoarray.plot.wrap.base.ticks import YTicks from autoarray.plot.wrap.base.ticks import XTicks @@ -261,15 +258,17 @@ def output_subplot(self, filename_suffix: str = ""): The suffix of the filename that the subplot is output to. """ - if self.plotter_list[0].mat_plot_1d is not None: - self.plotter_list[0].mat_plot_1d.output.subplot_to_figure( + plotter = self.plotter_list[0] + + if plotter.mat_plot_1d is not None: + plotter.mat_plot_1d.output.subplot_to_figure( auto_filename=f"subplot_{filename_suffix}" ) - if self.plotter_list[0].mat_plot_2d is not None: - self.plotter_list[0].mat_plot_2d.output.subplot_to_figure( + if plotter.mat_plot_2d is not None: + plotter.mat_plot_2d.output.subplot_to_figure( auto_filename=f"subplot_{filename_suffix}" ) - self.plotter_list[0].close_subplot_figure() + plotter.close_subplot_figure() def output_to_fits( self, @@ -316,10 +315,10 @@ def output_to_fits( output_path = self.plotter_list[0].mat_plot_2d.output.output_path_from( format="fits_multi" ) - output_fits_file = os.path.join(output_path, f"{filename}.fits") + output_fits_file = Path(output_path)/ f"{filename}.fits" - if remove_fits_first and os.path.exists(output_fits_file): - os.remove(output_fits_file) + if remove_fits_first: + output_fits_file.unlink(missing_ok=True) for i, plotter in enumerate(self.plotter_list): plotter.mat_plot_2d.output._format = "fits_multi" diff --git a/autoarray/structures/arrays/array_2d_util.py b/autoarray/structures/arrays/array_2d_util.py index 601cd9f48..1b9b902fc 100644 --- a/autoarray/structures/arrays/array_2d_util.py +++ b/autoarray/structures/arrays/array_2d_util.py @@ -898,8 +898,9 @@ def update_fits_file( header = fits.Header() try: - header["PIXSCAY"] = str(arr.pixel_scales[0]) - header["PIXSCAX"] = str(arr.pixel_scales[1]) + y, x = map(str, arr.pixel_scales) + header["PIXSCAY"] = y + header["PIXSCAX"] = x except AttributeError: pass