import logging
import numpy as np
from typing import Optional, List
import autoarray as aa
import autogalaxy as ag
from autogalaxy.util.plot_utils import plot_array
from autoarray.plot.array import _zoom_array_2d
from autoarray.plot.utils import subplots, save_figure, hide_unused_axes, conf_subplot_figsize, tight_layout, plot_regions
from autoarray.inversion.mappers.abstract import Mapper
from autoarray.inversion.plot.mapper_plots import plot_mapper
from autogalaxy.util.plot_utils import _critical_curves_from
# Canonical, single-source definition. Re-exported here so existing
# ``from autolens.imaging.plot.fit_imaging_plots import _compute_critical_curve_lines``
# imports keep working. It lives under autolens.lens.plot (empty package
# __init__) rather than autolens.plot to avoid a circular import through the
# autolens.plot aggregator __init__.
from autolens.lens.plot.critical_curves import _compute_critical_curve_lines
logger = logging.getLogger(__name__)
def _compute_critical_curves_from_fit(fit):
"""Compute critical-curve and caustic lines from a FitImaging object.
Convenience wrapper around :func:`_compute_critical_curve_lines` that
derives the tracer and grid from *fit* directly, using the fully unmasked
image-plane grid so the curves cover the whole image extent.
Returns the same 4-tuple as :func:`_compute_critical_curve_lines`.
"""
tracer = fit.tracer_linear_light_profiles_to_light_profiles
return _compute_critical_curve_lines(tracer, fit.mask.derive_grid.all_false)
def _get_source_vmax(fit):
"""
Return the colour-scale maximum for source-plane panels.
Computes the global maximum pixel value across all source-plane model
images (planes with index >= 1), so that source and subtracted panels
share a common colour scale. Returns ``None`` when no source-plane
model images exist (e.g. a lens-only fit) so callers can fall back to
automatic scaling.
Parameters
----------
fit : FitImaging
The imaging fit whose ``model_images_of_planes_list`` is inspected.
Returns
-------
float or None
Global maximum pixel value of all source-plane model images, or
``None`` if none are available.
"""
try:
return float(np.max([mi.array for mi in fit.model_images_of_planes_list[1:]]))
except (ValueError, IndexError):
return None
from autolens.lens.plot.tracer_plots import plane_image_from
def _plot_source_plane(fit, ax, plane_index, zoom_to_brightest=True,
colormap=None, use_log10=False, title=None,
lines=None, line_colors=None, vmax=None,
zoom_extent_scale: float = 1.0,
regions=None, region_alpha: float = 0.25,
region_labels=None):
"""
Plot the source-plane image (or a blank inversion placeholder) into an axes.
When the plane at ``plane_index`` does not contain a
`~autoarray.Pixelization` (i.e. it is a parametric source), the source
galaxy light profiles are evaluated on a plain uniform grid
(``fit.mask.derive_grid.all_false``) — **not** a ray-traced grid. This
shows the source as it appears in its own plane, without lensing
distortion. :func:`~autolens.lens.plot.tracer_plots.plane_image_from`
handles the optional zoom to the brightest region. When the plane *does*
contain a pixelization (an inversion source), the source reconstruction
is rendered via :func:`~autoarray.inversion.plot.mapper_plots.plot_mapper`
using ``zoom_to_brightest`` to control whether the view is zoomed in on
the brightest pixels or shown at full extent.
Parameters
----------
fit : FitImaging
The imaging fit providing the tracer, mask, and inversion state.
ax : matplotlib.axes.Axes or None
The axes into which the source-plane image is drawn. Passing
``None`` is a no-op.
plane_index : int
Index of the plane in ``fit.tracer.planes`` to visualise.
zoom_to_brightest : bool, optional
For parametric sources, zooms the evaluation grid in on the brightest
region of the source plane via :func:`plane_image_from`. For inversion
sources, zooms the colormap extent to the brightest reconstructed pixels.
colormap : str, optional
Matplotlib colormap name.
use_log10 : bool, optional
If ``True`` the colour scale is applied on a log10 stretch.
regions : list, optional
Source-plane regions to overlay as filled polygons, each a list of ``(N, 2)``
``(y, x)`` arrays -- e.g. the ``source_contours`` of a
`~autoarray.inversion.mappings.mapping.Mapping`. Drawn identically on the
parametric and the inversion path, so a mapping keeps its colour across both.
region_alpha : float, optional
The alpha of each region's fill.
region_labels : list of str, optional
A label drawn at the centre of each region, matching its image-plane counterpart.
"""
tracer = fit.tracer_linear_light_profiles_to_light_profiles
if not tracer.planes[plane_index].has(cls=aa.Pixelization):
if zoom_to_brightest:
grid = fit.mask.derive_grid.all_false
else:
zoom = aa.Zoom2D(mask=fit.mask)
grid = aa.Grid2D.from_extent(
extent=zoom.extent_from(buffer=0),
shape_native=zoom.shape_native,
)
zoom_extent_bounds = None
if zoom_extent_scale != 1.0:
zoom = aa.Zoom2D(mask=fit.mask)
no_zoom_grid = aa.Grid2D.from_extent(
extent=zoom.extent_from(buffer=0),
shape_native=zoom.shape_native,
)
zoom_extent_bounds = no_zoom_grid.geometry.extent
image = plane_image_from(
galaxies=tracer.planes[plane_index],
grid=grid,
zoom_to_brightest=zoom_to_brightest,
zoom_extent_scale=zoom_extent_scale,
zoom_extent_bounds=zoom_extent_bounds,
)
plot_array(
array=image, ax=ax,
title=title if title is not None else f"Source Plane {plane_index}",
colormap=colormap, use_log10=use_log10, vmax=vmax, lines=lines,
line_colors=line_colors,
)
# `autogalaxy.util.plot_utils.plot_array` is a thin wrapper which does not forward
# `regions=`; the overlay is drawn onto the same axes afterwards instead, using the
# same `plot_regions` the pixelized path below reaches through `plot_mapper`. The
# polygons are arcsec `(y, x)`, so they land correctly whatever zoom the wrapper
# applied.
plot_regions(ax=ax, regions=regions, region_alpha=region_alpha,
region_labels=region_labels)
else:
# Inversion path: plot the source reconstruction via the mapper.
try:
inversion = fit.inversion
mapper_list = inversion.cls_list_from(cls=Mapper)
mapper = mapper_list[plane_index - 1] if plane_index > 0 else mapper_list[0]
pixel_values = inversion.reconstruction_dict[mapper]
plot_mapper(
mapper,
solution_vector=pixel_values,
ax=ax,
title=title if title is not None else f"Source Reconstruction (plane {plane_index})",
colormap=colormap,
use_log10=use_log10,
vmax=vmax,
zoom_to_brightest=zoom_to_brightest,
zoom_extent_scale=zoom_extent_scale,
lines=lines,
line_colors=line_colors,
regions=regions,
region_alpha=region_alpha,
region_labels=region_labels,
)
except Exception as exc:
logger.warning(f"Could not plot source reconstruction for plane {plane_index}: {exc}")
if ax is not None:
ax.axis("off")
ax.set_title(f"Source Reconstruction (plane {plane_index})")
def subplot_fit(
fit,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
plane_index: Optional[int] = None,
image_plane_lines=None,
image_plane_line_colors=None,
source_plane_lines=None,
source_plane_line_colors=None,
title_prefix: str = None,
):
"""
Produce a 12-panel subplot summarising an imaging fit.
Arranges the following panels in a 3 × 4 grid:
* Data
* Model image
* Signal-to-noise map
* Source plane image (max zoom)
* Lens-light model image
* Lens-light-subtracted image (source scale)
* Source model image (source scale)
* Source plane image (mid zoom — 2× wider than max zoom, square, shrunk
uniformly so all edges stay inside the no-zoom extent)
* Normalised residual map (symmetric scale)
* Normalised residual map clipped to ± 1 σ
* Chi-squared map
* Source plane image (full extent)
For single-plane tracers the function delegates to
:func:`subplot_fit_x1_plane`, which uses a simpler 2 × 3 layout.
Parameters
----------
fit : FitImaging
The imaging fit to visualise.
output_path : str, optional
Directory in which to save the figure. If ``None`` the figure is
not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`
(e.g. ``"png"``, ``"pdf"``).
colormap : str, optional
Matplotlib colormap name applied to all image panels.
plane_index : int, optional
Index of the source plane to use for the source-scale panels.
Defaults to the final plane in the tracer.
"""
if len(fit.tracer.planes) == 1:
return subplot_fit_x1_plane(fit, output_path=output_path,
output_format=output_format, colormap=colormap,
title_prefix=title_prefix)
plane_index_tag = "" if plane_index is None else f"_{plane_index}"
final_plane_index = (
len(fit.tracer.planes) - 1 if plane_index is None else plane_index
)
source_vmax = _get_source_vmax(fit)
if image_plane_lines is None and source_plane_lines is None:
image_plane_lines, image_plane_line_colors, source_plane_lines, source_plane_line_colors = (
_compute_critical_curves_from_fit(fit)
)
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
fig, axes = subplots(3, 4, figsize=conf_subplot_figsize(3, 4))
axes_flat = list(axes.flatten())
plot_array(array=fit.data, ax=axes_flat[0], title=_pf("Data"), colormap=colormap)
plot_array(array=fit.model_data, ax=axes_flat[1], title=_pf("Model Image"),
colormap=colormap, lines=image_plane_lines,
line_colors=image_plane_line_colors)
plot_array(array=fit.signal_to_noise_map, ax=axes_flat[2],
title=_pf("Signal-To-Noise Map"), colormap=colormap)
# Source plane (max zoom)
_plot_source_plane(fit, axes_flat[3], final_plane_index, zoom_to_brightest=True,
colormap=colormap, title=_pf("Source Plane (Max Zoom)"),
lines=source_plane_lines, line_colors=source_plane_line_colors,
vmax=source_vmax)
# Lens model image
try:
lens_model_img = fit.model_images_of_planes_list[0]
except (IndexError, AttributeError):
lens_model_img = None
if lens_model_img is not None:
plot_array(array=lens_model_img, ax=axes_flat[4],
title=_pf("Lens Light Model Image"), colormap=colormap)
else:
axes_flat[4].axis("off")
# Subtracted image at source scale
try:
subtracted_img = fit.subtracted_images_of_planes_list[final_plane_index]
except (IndexError, AttributeError):
subtracted_img = None
if subtracted_img is not None:
plot_array(array=subtracted_img, ax=axes_flat[5], title=_pf("Lens Light Subtracted"),
colormap=colormap, vmin=0.0 if source_vmax is not None else None,
vmax=source_vmax)
else:
axes_flat[5].axis("off")
# Source model image at source scale
try:
source_model_img = fit.model_images_of_planes_list[final_plane_index]
except (IndexError, AttributeError):
source_model_img = None
if source_model_img is not None:
plot_array(array=source_model_img, ax=axes_flat[6], title=_pf("Source Model Image"),
colormap=colormap, vmax=source_vmax, lines=image_plane_lines,
line_colors=image_plane_line_colors)
else:
axes_flat[6].axis("off")
# Source plane (mid zoom) — same centre as Max Zoom, 2.5x wider extent
_plot_source_plane(fit, axes_flat[7], final_plane_index, zoom_to_brightest=True,
colormap=colormap, title=_pf("Source Plane (Mid Zoom)"),
lines=source_plane_lines, line_colors=source_plane_line_colors,
vmax=source_vmax, zoom_extent_scale=2.0)
# Normalized residual map (symmetric)
norm_resid = fit.normalized_residual_map
_abs_max = _symmetric_vmax(norm_resid)
plot_array(array=norm_resid, ax=axes_flat[8], title=_pf("Normalized Residual Map"),
colormap=colormap, vmin=-_abs_max, vmax=_abs_max)
# Normalized residual map clipped to [-1, 1]
plot_array(array=norm_resid, ax=axes_flat[9],
title=_pf(r"Normalized Residual Map $1\sigma$"),
colormap=colormap, vmin=-1.0, vmax=1.0)
plot_array(array=fit.chi_squared_map, ax=axes_flat[10],
title=_pf("Chi-Squared Map"), colormap=colormap, cb_unit=r"$\chi^2$")
# Source plane not zoomed
_plot_source_plane(fit, axes_flat[11], final_plane_index, zoom_to_brightest=False,
colormap=colormap, title=_pf("Source Plane (No Zoom)"),
lines=source_plane_lines, line_colors=source_plane_line_colors,
vmax=source_vmax)
hide_unused_axes(axes_flat)
tight_layout()
save_figure(fig, path=output_path, filename=f"fit{plane_index_tag}", format=output_format)
def subplot_fit_x1_plane(
fit,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
title_prefix: str = None,
):
"""
Produce a 6-panel subplot for a single-plane tracer imaging fit.
Arranges the following panels in a 2 × 3 grid:
* Data
* Signal-to-noise map
* Model image
* Normalised residual map (lens-light subtracted proxy)
* Normalised residual map with zero minimum
* Normalised residual map (symmetric scale)
This simplified layout is used automatically by :func:`subplot_fit`
when the tracer has only one plane (no source plane).
Parameters
----------
fit : FitImaging
The single-plane imaging fit to visualise.
output_path : str, optional
Directory in which to save the figure. If ``None`` the figure is
not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
"""
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
fig, axes = subplots(2, 3, figsize=conf_subplot_figsize(2, 3))
axes_flat = list(axes.flatten())
try:
vmax = float(np.max(fit.model_images_of_planes_list[0].array))
except (IndexError, AttributeError, ValueError):
vmax = None
plot_array(array=fit.data, ax=axes_flat[0], title=_pf("Data"), colormap=colormap, vmax=vmax)
plot_array(array=fit.signal_to_noise_map, ax=axes_flat[1],
title=_pf("Signal-To-Noise Map"), colormap=colormap)
plot_array(array=fit.model_data, ax=axes_flat[2], title=_pf("Model Image"),
colormap=colormap, vmax=vmax)
norm_resid = fit.normalized_residual_map
plot_array(array=norm_resid, ax=axes_flat[3], title=_pf("Lens Light Subtracted"),
colormap=colormap, cb_unit=r"$\sigma$")
plot_array(array=norm_resid, ax=axes_flat[4], title=_pf("Subtracted Image Zero Minimum"),
colormap=colormap, vmin=0.0, cb_unit=r"$\sigma$")
_abs_max = _symmetric_vmax(norm_resid)
plot_array(array=norm_resid, ax=axes_flat[5], title=_pf("Normalized Residual Map"),
colormap=colormap, vmin=-_abs_max, vmax=_abs_max, cb_unit=r"$\sigma$")
tight_layout()
save_figure(fig, path=output_path, filename="fit_x1_plane", format=output_format)
def subplot_fit_log10(
fit,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
plane_index: Optional[int] = None,
image_plane_lines=None,
image_plane_line_colors=None,
source_plane_lines=None,
source_plane_line_colors=None,
title_prefix: str = None,
):
"""
Produce a 12-panel subplot summarising an imaging fit with log10 colour scaling.
Equivalent to :func:`subplot_fit` but applies a log10 stretch to all
positive-valued panels (data, model image, lens-light model, subtracted
image, source model image, chi-squared map, source plane images).
Residual panels are left on a linear scale because they contain negative
values. Includes Source Plane (Max Zoom) and Source Plane (Mid Zoom)
panels — Mid Zoom shares the Max Zoom centre with extents 2x larger,
kept square and shrunk uniformly so all edges stay inside the No Zoom
extent.
For single-plane tracers the function delegates to
:func:`subplot_fit_log10_x1_plane`.
Parameters
----------
fit : FitImaging
The imaging fit to visualise.
output_path : str, optional
Directory in which to save the figure. If ``None`` the figure is
not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
plane_index : int, optional
Index of the source plane to use for the source-scale panels.
Defaults to the final plane in the tracer.
"""
if len(fit.tracer.planes) == 1:
return subplot_fit_log10_x1_plane(fit, output_path=output_path,
output_format=output_format, colormap=colormap,
title_prefix=title_prefix)
plane_index_tag = "" if plane_index is None else f"_{plane_index}"
final_plane_index = (
len(fit.tracer.planes) - 1 if plane_index is None else plane_index
)
source_vmax = _get_source_vmax(fit)
if image_plane_lines is None and source_plane_lines is None:
image_plane_lines, image_plane_line_colors, source_plane_lines, source_plane_line_colors = (
_compute_critical_curves_from_fit(fit)
)
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
fig, axes = subplots(3, 4, figsize=conf_subplot_figsize(3, 4))
axes_flat = list(axes.flatten())
plot_array(array=fit.data, ax=axes_flat[0], title=_pf("Data"), colormap=colormap,
use_log10=True)
plot_array(array=fit.model_data, ax=axes_flat[1], title=_pf("Model Image"),
colormap=colormap, use_log10=True, lines=image_plane_lines,
line_colors=image_plane_line_colors)
try:
plot_array(array=fit.signal_to_noise_map, ax=axes_flat[2],
title=_pf("Signal-To-Noise Map"), colormap=colormap, use_log10=True)
except ValueError:
axes_flat[2].axis("off")
# Source plane (max zoom)
_plot_source_plane(fit, axes_flat[3], final_plane_index, zoom_to_brightest=True,
colormap=colormap, use_log10=True,
title=_pf("Source Plane (Max Zoom)"),
lines=source_plane_lines, line_colors=source_plane_line_colors,
vmax=source_vmax)
try:
lens_model_img = fit.model_images_of_planes_list[0]
plot_array(array=lens_model_img, ax=axes_flat[4],
title=_pf("Lens Light Model Image"), colormap=colormap, use_log10=True)
except (IndexError, AttributeError):
axes_flat[4].axis("off")
try:
subtracted_img = fit.subtracted_images_of_planes_list[final_plane_index]
plot_array(array=subtracted_img, ax=axes_flat[5],
title=_pf("Lens Light Subtracted"), colormap=colormap, use_log10=True)
except (IndexError, AttributeError):
axes_flat[5].axis("off")
try:
source_model_img = fit.model_images_of_planes_list[final_plane_index]
plot_array(array=source_model_img, ax=axes_flat[6],
title=_pf("Source Model Image"), colormap=colormap, use_log10=True,
lines=image_plane_lines, line_colors=image_plane_line_colors)
except (IndexError, AttributeError):
axes_flat[6].axis("off")
# Source plane (mid zoom) — same centre as Max Zoom, 2.5x wider extent
_plot_source_plane(fit, axes_flat[7], final_plane_index, zoom_to_brightest=True,
colormap=colormap, use_log10=True,
title=_pf("Source Plane (Mid Zoom)"),
lines=source_plane_lines, line_colors=source_plane_line_colors,
vmax=source_vmax, zoom_extent_scale=2.0)
norm_resid = fit.normalized_residual_map
_abs_max = _symmetric_vmax(norm_resid)
plot_array(array=norm_resid, ax=axes_flat[8], title=_pf("Normalized Residual Map"),
colormap=colormap, vmin=-_abs_max, vmax=_abs_max, cb_unit=r"$\sigma$")
plot_array(array=norm_resid, ax=axes_flat[9],
title=_pf(r"Normalized Residual Map $1\sigma$"),
colormap=colormap, vmin=-1.0, vmax=1.0, cb_unit=r"$\sigma$")
plot_array(array=fit.chi_squared_map, ax=axes_flat[10], title=_pf("Chi-Squared Map"),
colormap=colormap, use_log10=True, cb_unit=r"$\chi^2$")
_plot_source_plane(fit, axes_flat[11], final_plane_index, zoom_to_brightest=False,
colormap=colormap, use_log10=True,
title=_pf("Source Plane (No Zoom)"),
lines=source_plane_lines, line_colors=source_plane_line_colors,
vmax=source_vmax)
tight_layout()
save_figure(fig, path=output_path, filename=f"fit_log10{plane_index_tag}", format=output_format)
def subplot_fit_log10_x1_plane(
fit,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
title_prefix: str = None,
):
"""
Produce a 6-panel log10 subplot for a single-plane tracer imaging fit.
Equivalent to :func:`subplot_fit_x1_plane` but applies a log10 colour
stretch to the data, model image, and chi-squared panels. Residual
panels remain on a linear scale.
This simplified layout is used automatically by
:func:`subplot_fit_log10` when the tracer has only one plane.
Parameters
----------
fit : FitImaging
The single-plane imaging fit to visualise.
output_path : str, optional
Directory in which to save the figure. If ``None`` the figure is
not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
"""
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
fig, axes = subplots(2, 3, figsize=conf_subplot_figsize(2, 3))
axes_flat = list(axes.flatten())
try:
vmax = float(np.max(fit.model_images_of_planes_list[0].array))
except (IndexError, AttributeError, ValueError):
vmax = None
plot_array(array=fit.data, ax=axes_flat[0], title=_pf("Data"), colormap=colormap,
vmax=vmax, use_log10=True)
try:
plot_array(array=fit.signal_to_noise_map, ax=axes_flat[1],
title=_pf("Signal-To-Noise Map"), colormap=colormap, use_log10=True)
except ValueError:
axes_flat[1].axis("off")
plot_array(array=fit.model_data, ax=axes_flat[2], title=_pf("Model Image"),
colormap=colormap, vmax=vmax, use_log10=True)
norm_resid = fit.normalized_residual_map
plot_array(array=norm_resid, ax=axes_flat[3], title=_pf("Lens Light Subtracted"),
colormap=colormap, cb_unit=r"$\sigma$")
_abs_max = _symmetric_vmax(norm_resid)
plot_array(array=norm_resid, ax=axes_flat[4], title=_pf("Normalized Residual Map"),
colormap=colormap, vmin=-_abs_max, vmax=_abs_max, cb_unit=r"$\sigma$")
plot_array(array=fit.chi_squared_map, ax=axes_flat[5], title=_pf("Chi-Squared Map"),
colormap=colormap, use_log10=True, cb_unit=r"$\chi^2$")
tight_layout()
save_figure(fig, path=output_path, filename="fit_log10", format=output_format)
def subplot_of_planes(
fit,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
plane_index: Optional[int] = None,
title_prefix: str = None,
):
"""
Produce a 4-panel subplot for each plane in the tracer.
For every plane (or the single plane specified by ``plane_index``), a
1 × 4 row is saved to its own figure containing:
* Data
* Lens-light-subtracted image for that plane
* Model image contributed by that plane
* Source-plane image evaluated at that plane
Each figure is saved with the filename
``subplot_of_plane_<plane_index>``.
Parameters
----------
fit : FitImaging
The imaging fit whose planes are visualised.
output_path : str, optional
Directory in which to save the figures. If ``None`` the figures
are not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
plane_index : int, optional
If provided, only the subplot for that specific plane is produced.
If ``None`` (default) a subplot is produced for every plane in the
tracer.
"""
if plane_index is None:
plane_indexes = range(len(fit.tracer.planes))
else:
plane_indexes = [plane_index]
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
for pidx in plane_indexes:
fig, axes = subplots(1, 4, figsize=conf_subplot_figsize(1, 4))
axes_flat = list(axes.flatten())
plot_array(array=fit.data, ax=axes_flat[0], title=_pf("Data"), colormap=colormap)
try:
subtracted = fit.subtracted_images_of_planes_list[pidx]
plot_array(array=subtracted, ax=axes_flat[1],
title=_pf(f"Subtracted Image Plane {pidx}"), colormap=colormap)
except (IndexError, AttributeError):
axes_flat[1].axis("off")
try:
model_img = fit.model_images_of_planes_list[pidx]
plot_array(array=model_img, ax=axes_flat[2],
title=_pf(f"Model Image Plane {pidx}"), colormap=colormap)
except (IndexError, AttributeError):
axes_flat[2].axis("off")
_plot_source_plane(fit, axes_flat[3], pidx, colormap=colormap,
title=_pf(f"Source Plane {pidx}"))
tight_layout()
save_figure(fig, path=output_path, filename=f"fit_of_plane_{pidx}", format=output_format)
def subplot_tracer_from_fit(
fit,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
image_plane_lines=None,
image_plane_line_colors=None,
source_plane_lines=None,
source_plane_line_colors=None,
title_prefix: str = None,
):
"""
Produce a 9-panel tracer subplot derived from a `FitImaging` object.
Panels (3x3 = 9 axes):
0: Model image with critical curves
1: Source model image (image-plane projection) with critical curves
2: Source plane (no zoom) with caustics
3: Lens image (log10) with critical curves
4: Convergence (log10)
5: Potential (log10)
6: Deflections Y with critical curves
7: Deflections X with critical curves
8: Magnification with critical curves
Parameters
----------
fit : FitImaging
The imaging fit whose best-fit tracer is visualised.
output_path : str, optional
Directory in which to save the figure. If ``None`` the figure is
not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
"""
from autogalaxy.operate.lens_calc import LensCalc
final_plane_index = len(fit.tracer.planes) - 1
tracer = fit.tracer_linear_light_profiles_to_light_profiles
# --- grid ---
grid = fit.mask.derive_grid.all_false
if image_plane_lines is None and source_plane_lines is None:
image_plane_lines, image_plane_line_colors, source_plane_lines, source_plane_line_colors = (
_compute_critical_curves_from_fit(fit)
)
source_vmax = _get_source_vmax(fit)
traced_grids = tracer.traced_grid_2d_list_from(grid=grid)
lens_galaxies = ag.Galaxies(galaxies=tracer.planes[0])
lens_image = lens_galaxies.image_2d_from(grid=traced_grids[0])
deflections = tracer.deflections_yx_2d_from(grid=grid)
deflections_y = aa.Array2D(values=deflections.slim[:, 0], mask=grid.mask)
deflections_x = aa.Array2D(values=deflections.slim[:, 1], mask=grid.mask)
magnification = LensCalc.from_mass_obj(tracer).magnification_2d_from(grid=grid)
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
fig, axes = subplots(3, 3, figsize=conf_subplot_figsize(3, 3))
axes_flat = list(axes.flatten())
# Panel 0: Model Image
plot_array(array=fit.model_data, ax=axes_flat[0], title=_pf("Model Image"),
lines=image_plane_lines, line_colors=image_plane_line_colors,
colormap=colormap)
# Panel 1: Source Model Image (same as subplot_fit panel 7)
try:
source_model_img = fit.model_images_of_planes_list[final_plane_index]
except (IndexError, AttributeError):
source_model_img = None
if source_model_img is not None:
plot_array(array=source_model_img, ax=axes_flat[1], title=_pf("Source Model Image"),
colormap=colormap, vmax=source_vmax,
lines=image_plane_lines, line_colors=image_plane_line_colors)
else:
axes_flat[1].axis("off")
# Panel 2: Source Plane (No Zoom) (same as subplot_fit panel 12)
_plot_source_plane(fit, axes_flat[2], final_plane_index, zoom_to_brightest=False,
colormap=colormap, title=_pf("Source Plane (No Zoom)"),
lines=source_plane_lines, line_colors=source_plane_line_colors,
vmax=source_vmax)
# Panel 3: Lens Image (log10)
plot_array(array=lens_image, ax=axes_flat[3], title=_pf("Lens Image"),
lines=image_plane_lines, line_colors=image_plane_line_colors,
colormap=colormap, use_log10=True)
# Panel 4: Convergence (log10)
try:
convergence = tracer.convergence_2d_from(grid=grid)
plot_array(array=convergence, ax=axes_flat[4], title=_pf("Convergence"),
colormap=colormap, use_log10=True)
except Exception:
axes_flat[4].axis("off")
# Panel 5: Potential (log10)
try:
potential = tracer.potential_2d_from(grid=grid)
plot_array(array=potential, ax=axes_flat[5], title=_pf("Potential"),
colormap=colormap, use_log10=True)
except Exception:
axes_flat[5].axis("off")
# Panel 6: Deflections Y
plot_array(array=deflections_y, ax=axes_flat[6], title=_pf("Deflections Y"),
colormap=colormap)
# Panel 7: Deflections X
plot_array(array=deflections_x, ax=axes_flat[7], title=_pf("Deflections X"),
colormap=colormap)
# Panel 8: Magnification
plot_array(array=magnification, ax=axes_flat[8], title=_pf("Magnification"),
colormap=colormap)
tight_layout()
save_figure(fig, path=output_path, filename="tracer", format=output_format)
[docs]
def subplot_fit_combined(
fit_list: List,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
title_prefix: str = None,
):
"""
Produce a combined multi-row subplot for a list of `FitImaging` objects.
Each row corresponds to one fit and contains six panels:
* Data
* Lens-light-subtracted image (plane 1)
* Lens model image (plane 0)
* Source model image (final plane)
* Source plane image (final plane)
* Normalised residual map
This layout is useful for visually comparing fits from multiple
datasets or epochs side by side.
Parameters
----------
fit_list : list of FitImaging
The imaging fits to display. Each fit occupies one row of the
figure.
output_path : str, optional
Directory in which to save the figure. If ``None`` the figure is
not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
"""
n_fits = len(fit_list)
n_cols = 6
fig, axes = subplots(n_fits, n_cols, figsize=conf_subplot_figsize(n_fits, n_cols))
if n_fits == 1:
all_axes = [list(axes)]
else:
all_axes = [list(axes[i]) for i in range(n_fits)]
final_plane_index = len(fit_list[0].tracer.planes) - 1
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
for row, fit in enumerate(fit_list):
row_axes = all_axes[row]
plot_array(array=fit.data, ax=row_axes[0], title=_pf("Data"), colormap=colormap)
try:
subtracted = fit.subtracted_images_of_planes_list[1]
plot_array(array=subtracted, ax=row_axes[1], title=_pf("Subtracted Image"),
colormap=colormap)
except (IndexError, AttributeError):
row_axes[1].axis("off")
try:
lens_model = fit.model_images_of_planes_list[0]
plot_array(array=lens_model, ax=row_axes[2], title=_pf("Lens Model Image"),
colormap=colormap)
except (IndexError, AttributeError):
row_axes[2].axis("off")
try:
source_model = fit.model_images_of_planes_list[final_plane_index]
plot_array(array=source_model, ax=row_axes[3], title=_pf("Source Model Image"),
colormap=colormap)
except (IndexError, AttributeError):
row_axes[3].axis("off")
try:
_plot_source_plane(fit, row_axes[4], final_plane_index, colormap=colormap,
title=_pf(f"Source Plane {final_plane_index}"))
except Exception:
row_axes[4].axis("off")
plot_array(array=fit.normalized_residual_map, ax=row_axes[5],
title=_pf("Normalized Residual Map"), colormap=colormap, cb_unit=r"$\sigma$")
tight_layout()
save_figure(fig, path=output_path, filename="fit_combined", format=output_format)
[docs]
def subplot_fit_combined_log10(
fit_list: List,
output_path: Optional[str] = None,
output_format: str = None,
colormap: Optional[str] = None,
title_prefix: str = None,
):
"""
Produce a combined log10 multi-row subplot for a list of `FitImaging` objects.
Equivalent to :func:`subplot_fit_combined` but applies a log10 colour
stretch to the data, lens model, and source model panels. The
normalised residual panel remains on a linear scale.
Parameters
----------
fit_list : list of FitImaging
The imaging fits to display. Each fit occupies one row of the
figure.
output_path : str, optional
Directory in which to save the figure. If ``None`` the figure is
not saved to disk.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
"""
n_fits = len(fit_list)
n_cols = 6
fig, axes = subplots(n_fits, n_cols, figsize=conf_subplot_figsize(n_fits, n_cols))
if n_fits == 1:
all_axes = [list(axes)]
else:
all_axes = [list(axes[i]) for i in range(n_fits)]
final_plane_index = len(fit_list[0].tracer.planes) - 1
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
for row, fit in enumerate(fit_list):
row_axes = all_axes[row]
plot_array(array=fit.data, ax=row_axes[0], title=_pf("Data"), colormap=colormap,
use_log10=True)
try:
subtracted = fit.subtracted_images_of_planes_list[1]
plot_array(array=subtracted, ax=row_axes[1], title=_pf("Subtracted Image"),
colormap=colormap, use_log10=True)
except (IndexError, AttributeError):
row_axes[1].axis("off")
try:
lens_model = fit.model_images_of_planes_list[0]
plot_array(array=lens_model, ax=row_axes[2], title=_pf("Lens Model Image"),
colormap=colormap, use_log10=True)
except (IndexError, AttributeError):
row_axes[2].axis("off")
try:
source_model = fit.model_images_of_planes_list[final_plane_index]
plot_array(array=source_model, ax=row_axes[3], title=_pf("Source Model Image"),
colormap=colormap, use_log10=True)
except (IndexError, AttributeError):
row_axes[3].axis("off")
try:
_plot_source_plane(fit, row_axes[4], final_plane_index, colormap=colormap,
use_log10=True,
title=_pf(f"Source Plane {final_plane_index}"))
except Exception:
row_axes[4].axis("off")
plot_array(array=fit.normalized_residual_map, ax=row_axes[5],
title=_pf("Normalized Residual Map"), colormap=colormap, cb_unit=r"$\sigma$")
tight_layout()
save_figure(fig, path=output_path, filename="fit_combined_log10", format=output_format)
def _symmetric_vmax(array) -> float:
"""
Return the absolute-maximum finite value for symmetric colormap scaling.
Zooms into the unmasked region of ``array``, extracts all finite pixel
values, and returns their absolute maximum. Used to set ``vmin`` and
``vmax`` symmetrically around zero for residual-map panels so that the
zero-residual colour is centred in the colormap.
Parameters
----------
array : Array2D or array-like
The array from which the symmetric colour limit is computed.
Returns
-------
float
The absolute maximum of all finite pixel values in the (zoomed)
array. Returns ``1.0`` if the array contains no finite values.
"""
try:
vals = _zoom_array_2d(array).native.array
except AttributeError:
vals = np.asarray(array)
finite = vals[np.isfinite(vals)]
return float(np.max(np.abs(finite))) if finite.size else 1.0
def subplot_mappings(
fit,
plane_index: int = -1,
shape=None,
output_path: Optional[str] = None,
output_filename: str = "mappings",
output_format: str = None,
colormap: Optional[str] = None,
threshold: Optional[float] = None,
min_pixels: Optional[int] = None,
total_clumps: Optional[int] = None,
pix_indexes: Optional[List] = None,
weight_threshold: float = 0.0,
pixel_scale_precision: Optional[float] = None,
magnification_threshold: float = 0.1,
region_alpha: float = 0.25,
image_plane_lines=None,
image_plane_line_colors=None,
source_plane_lines=None,
source_plane_line_colors=None,
title_prefix: str = None,
):
"""
2x2 subplot showing, in one look, how the brightest regions of the source map to the
image plane and back.
The panels are:
* (0, 0) the lens-light-subtracted data, with the image-plane regions and the critical
curves overlaid;
* (0, 1) the model image of the source plane, with the same regions;
* (1, 0) the source plane zoomed to its brightest region, with the source regions and
the caustics;
* (1, 1) the source plane at full extent, with the same.
Every source region is filled in its own colour and numbered, and the multiple images
it maps to carry the same colour and the same number in the image-plane panels -- so a
reader can read a merging pair of source galaxies off the figure by colour alone.
This is the fit-level counterpart of
:func:`~autoarray.inversion.plot.inversion_plots.subplot_mappings`, which works only
for a pixelized source. It works for **either** kind of source: a plane which
``has(cls=aa.Pixelization)`` is mapped via the inversion's clumps, and a parametric
plane via `ShapeSolver`'s ray-traced triangles. See
:mod:`autolens.lens.mappings` for the two engines.
Parameters
----------
fit : FitImaging
The imaging fit to visualise.
plane_index : int, optional
The index of the source plane whose mappings are drawn. Defaults to the last plane.
shape : Shape, optional
The source-plane region for a parametric source. ``None`` uses a `Circle` at the
first source light profile's centre, with its half-light radius. Ignored for a
pixelized source, which finds its own clumps.
output_path : str, optional
Directory in which to save the figure. ``None`` does not save it.
output_filename : str, optional
The filename stem; ``_{plane_index}`` is appended, as the sibling subplots do.
output_format : str, optional
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
colormap : str, optional
Matplotlib colormap name applied to all image panels.
threshold, min_pixels, total_clumps, pix_indexes, weight_threshold
The clump-finding controls of the pixelized (engine A) path, passed through to
`Inversion.mappings_from`. ``threshold`` is the knob which decides what counts as
a distinct source structure: ~0.5 isolates one smooth source, ~0.2 merges two
nearby galaxies, ~0.8 splits a source into its star-forming knots.
pixel_scale_precision, magnification_threshold
The solver controls of the parametric (engine B) path.
region_alpha : float, optional
The alpha of each drawn region's fill.
image_plane_lines, image_plane_line_colors, source_plane_lines, source_plane_line_colors
Pre-computed critical curves and caustics; computed from the fit when not given.
title_prefix : str, optional
A string prefixed to every panel title.
"""
from autolens.lens import mappings as mappings_module
plane_index_tag = len(fit.tracer.planes) - 1 if plane_index == -1 else plane_index
clump_kwargs = {}
if threshold is not None:
clump_kwargs["threshold"] = threshold
if min_pixels is not None:
clump_kwargs["min_pixels"] = min_pixels
if total_clumps is not None:
clump_kwargs["total_clumps"] = total_clumps
if pix_indexes is not None:
clump_kwargs["pix_indexes"] = pix_indexes
# No guard around this call. A source plane which cannot be mapped -- a plane with
# neither a pixelization nor a light profile, a profile with no half-light radius, a
# singular inversion -- raises, naming what the caller has to change. Swallowing that
# would draw the four panels with no regions on them, which reads as "this source has
# no multiple images" and is a worse outcome than a traceback. The one case which
# legitimately produces nothing to draw is an empty `mappings` list (below), which is
# a real answer rather than a failure.
mappings = mappings_module.mappings_from_fit(
fit=fit,
plane_index=plane_index,
shape=shape,
pixel_scale_precision=pixel_scale_precision,
magnification_threshold=magnification_threshold,
weight_threshold=weight_threshold,
**clump_kwargs,
)
image_regions = [mapping.image_contours for mapping in mappings]
source_regions = [mapping.source_contours for mapping in mappings]
region_labels = [str(i + 1) for i in range(len(mappings))]
if image_plane_lines is None and source_plane_lines is None:
(
image_plane_lines,
image_plane_line_colors,
source_plane_lines,
source_plane_line_colors,
) = _compute_critical_curves_from_fit(fit)
source_vmax = _get_source_vmax(fit)
_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_flat = list(axes.flatten())
# panel (0, 0): the data with everything but this plane subtracted.
try:
subtracted_image = fit.subtracted_images_of_planes_list[plane_index]
except (IndexError, AttributeError):
subtracted_image = None
if subtracted_image is not None:
plot_array(
array=subtracted_image,
ax=axes_flat[0],
title=_pf("Lens Light Subtracted"),
colormap=colormap,
vmin=0.0 if source_vmax is not None else None,
vmax=source_vmax,
lines=image_plane_lines,
line_colors=image_plane_line_colors,
)
plot_regions(
ax=axes_flat[0],
regions=image_regions,
region_alpha=region_alpha,
region_labels=region_labels,
)
else:
axes_flat[0].axis("off")
# panel (0, 1): the model image of the plane, with the same regions.
try:
model_image = fit.model_images_of_planes_list[plane_index]
except (IndexError, AttributeError):
model_image = None
if model_image is not None:
plot_array(
array=model_image,
ax=axes_flat[1],
title=_pf("Source Model Image"),
colormap=colormap,
vmax=source_vmax,
lines=image_plane_lines,
line_colors=image_plane_line_colors,
)
plot_regions(
ax=axes_flat[1],
regions=image_regions,
region_alpha=region_alpha,
region_labels=region_labels,
)
else:
axes_flat[1].axis("off")
# panels (1, 0) and (1, 1): the source plane, zoomed and unzoomed.
for ax, zoom, title in (
(axes_flat[2], True, "Source Plane (Zoom)"),
(axes_flat[3], False, "Source Plane (No Zoom)"),
):
_plot_source_plane(
fit,
ax,
plane_index,
zoom_to_brightest=zoom,
colormap=colormap,
title=_pf(title),
lines=source_plane_lines,
line_colors=source_plane_line_colors,
vmax=source_vmax,
regions=source_regions,
region_alpha=region_alpha,
region_labels=region_labels,
)
hide_unused_axes(axes_flat)
tight_layout()
save_figure(
fig,
path=output_path,
filename=f"{output_filename}_{plane_index_tag}",
format=output_format,
)