diff --git a/autoarray/config/visualize/general.yaml b/autoarray/config/visualize/general.yaml index 3c65121e..119aa3e9 100644 --- a/autoarray/config/visualize/general.yaml +++ b/autoarray/config/visualize/general.yaml @@ -1,5 +1,6 @@ general: backend: default # The matplotlib backend used for visualization. `default` uses the system default, can specify specific backend (e.g. TKAgg, Qt5Agg, WXAgg). + dpi: 150 # Resolution in dots per inch used when saving figures. Lower values reduce file size (e.g. 150 gives ~50% smaller files than 300 with negligible quality loss for diagnostic subplots). imshow_origin: upper # The `origin` input of `imshow`, determining if pixel values are ascending or descending on the y-axis. log10_min_value: 1.0e-4 # If negative values are being plotted on a log10 scale, values below this value are rounded up to it (e.g. to remove negative values). log10_max_value: 1.0e99 # If positive values are being plotted on a log10 scale, values above this value are rounded down to it. diff --git a/autoarray/inversion/plot/inversion_plots.py b/autoarray/inversion/plot/inversion_plots.py index d91402a3..ea4c174b 100644 --- a/autoarray/inversion/plot/inversion_plots.py +++ b/autoarray/inversion/plot/inversion_plots.py @@ -116,13 +116,18 @@ def _recon_array(): # panels 4-5: source reconstruction zoomed / unzoomed pixel_values = inversion.reconstruction_dict[mapper] + try: + recon_vmax = float(np.max(np.asarray(_recon_array()))) + except Exception: + recon_vmax = None plot_mapper( mapper, solution_vector=pixel_values, ax=axes[4], - title="Source Reconstruction", + title="Source Plane (Zoom)", colormap=colormap, use_log10=use_log10, + vmax=recon_vmax, zoom_to_brightest=True, mesh_grid=mesh_grid, lines=lines, @@ -131,9 +136,10 @@ def _recon_array(): mapper, solution_vector=pixel_values, ax=axes[5], - title="Source Reconstruction (Unzoomed)", + title="Source Plane (No Zoom)", colormap=colormap, use_log10=use_log10, + vmax=recon_vmax, zoom_to_brightest=False, mesh_grid=mesh_grid, lines=lines, @@ -316,7 +322,7 @@ def subplot_mappings( mapper, solution_vector=pixel_values, ax=axes[2], - title="Source Reconstruction", + title="Source Plane (Zoom)", colormap=colormap, use_log10=use_log10, zoom_to_brightest=True, @@ -327,7 +333,7 @@ def subplot_mappings( mapper, solution_vector=pixel_values, ax=axes[3], - title="Source Reconstruction (Unzoomed)", + title="Source Plane (No Zoom)", colormap=colormap, use_log10=use_log10, zoom_to_brightest=False, diff --git a/autoarray/inversion/plot/mapper_plots.py b/autoarray/inversion/plot/mapper_plots.py index 3dff6c19..8d5a7fdb 100644 --- a/autoarray/inversion/plot/mapper_plots.py +++ b/autoarray/inversion/plot/mapper_plots.py @@ -18,6 +18,8 @@ def plot_mapper( output_format: str = "png", colormap=None, use_log10: bool = False, + vmin=None, + vmax=None, mesh_grid=None, lines=None, line_colors=None, @@ -63,6 +65,8 @@ def plot_mapper( title=title, colormap=colormap, use_log10=use_log10, + vmin=vmin, + vmax=vmax, zoom_to_brightest=zoom_to_brightest, lines=numpy_lines(lines), line_colors=line_colors, diff --git a/autoarray/plot/inversion.py b/autoarray/plot/inversion.py index c8b6bbff..3a245d55 100644 --- a/autoarray/plot/inversion.py +++ b/autoarray/plot/inversion.py @@ -119,7 +119,7 @@ def plot_inversion_reconstruction( elif isinstance( mapper.interpolator, (InterpolatorDelaunay, InterpolatorKNearestNeighbor) ): - _plot_delaunay(ax, pixel_values, mapper, norm, colormap, is_subplot=is_subplot) + _plot_delaunay(ax, pixel_values, mapper, norm, colormap, extent, is_subplot=is_subplot) # --- overlays -------------------------------------------------------------- if lines is not None: @@ -230,7 +230,7 @@ def _plot_rectangular(ax, pixel_values, mapper, norm, colormap, extent, is_subpl _apply_colorbar(im, ax, is_subplot=is_subplot) -def _plot_delaunay(ax, pixel_values, mapper, norm, colormap, is_subplot=False): +def _plot_delaunay(ax, pixel_values, mapper, norm, colormap, extent, is_subplot=False): """Render a Delaunay or KNN pixelization reconstruction onto *ax*. Uses ``ax.tripcolor`` with Gouraud shading so that the reconstructed @@ -252,10 +252,19 @@ def _plot_delaunay(ax, pixel_values, mapper, norm, colormap, is_subplot=False): ``None`` for automatic scaling. colormap Matplotlib colormap name. + extent + ``[xmin, xmax, ymin, ymax]`` spatial extent; used to set the axes + aspect ratio to match rectangular pixelization plots. is_subplot When ``True`` uses ``labelsize_subplot`` from config for the colorbar tick labels (matches the behaviour of :func:`~autoarray.plot.array.plot_array`). """ + xmin, xmax, ymin, ymax = extent + x_range = abs(xmax - xmin) + y_range = abs(ymax - ymin) + box_aspect = (x_range / y_range) if y_range > 0 else 1.0 + ax.set_aspect(box_aspect, adjustable="box") + mesh_grid = mapper.source_plane_mesh_grid if hasattr(mesh_grid, "array"): diff --git a/autoarray/plot/utils.py b/autoarray/plot/utils.py index 27d0f5c2..9e72d2b0 100644 --- a/autoarray/plot/utils.py +++ b/autoarray/plot/utils.py @@ -419,7 +419,7 @@ def save_figure( path: str, filename: str, format: str = "png", - dpi: int = 300, + dpi: Optional[int] = None, structure=None, ) -> None: """ @@ -448,6 +448,10 @@ def save_figure( of ``fig.savefig``. Callers do not need to pass this; ``plot_array`` supplies it automatically from the input array. """ + if dpi is None: + from autoconf import conf + dpi = int(conf.instance["visualize"]["general"]["general"]["dpi"]) + if path: os.makedirs(path, exist_ok=True) formats = format if isinstance(format, (list, tuple)) else [format] @@ -717,11 +721,20 @@ def _inward_ticks(lo: float, hi: float, factor: float, n: int) -> np.ndarray: def _round_ticks(values: np.ndarray, sig: int = 2) -> np.ndarray: - """Round *values* to *sig* significant figures.""" + """Round *values* to *sig* significant figures. + + After rounding, values smaller than 1e-10 of the overall tick scale are + clamped to zero so that floating-point noise (e.g. 1e-16 centre ticks on + symmetric extents) does not appear as scientific notation in labels. + """ with np.errstate(divide="ignore", invalid="ignore"): nonzero = np.where(values != 0, np.abs(values), 1.0) mags = np.where(values != 0, 10 ** (sig - 1 - np.floor(np.log10(nonzero))), 1.0) - return np.round(values * mags) / mags + rounded = np.round(values * mags) / mags + scale = float(np.max(np.abs(rounded))) if len(rounded) > 0 else 1.0 + if scale > 0: + rounded[np.abs(rounded) < scale * 1e-10] = 0.0 + return rounded def _arcsec_labels(ticks) -> List[str]: