Skip to content

Commit d32c696

Browse files
authored
Merge pull request #494 from PyAutoLabs/feature/alma-datacube
Add VisualizerInterferometer combined plotter for datacube fits
2 parents b0587e5 + 5a8a7dc commit d32c696

3 files changed

Lines changed: 187 additions & 0 deletions

File tree

autolens/interferometer/model/plotter.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from autolens.interferometer.plot.fit_interferometer_plots import (
1414
subplot_fit,
1515
subplot_fit_dirty_images,
16+
subplot_fit_interferometer_combined,
1617
subplot_fit_real_space,
1718
subplot_tracer_from_fit,
1819
_compute_critical_curve_lines,
@@ -106,3 +107,40 @@ def should_plot(name):
106107

107108
if should_plot("fits_dirty_images"):
108109
fits_dirty_images(fit=fit, output_path=self.image_path)
110+
111+
def fit_interferometer_combined(
112+
self,
113+
fit_list,
114+
quick_update: bool = False,
115+
):
116+
"""
117+
Output visualization of all `FitInterferometer` objects in a summed combined
118+
analysis (e.g. an ALMA datacube modelled as a list of channels via
119+
`af.FactorGraphModel`).
120+
121+
Outputs ``fit_combined.png`` in the visualisation directory: a row-per-channel
122+
subplot showing dirty image, dirty model image, source-plane reconstruction
123+
and dirty normalised residual map for every channel side by side.
124+
125+
Parameters
126+
----------
127+
fit_list
128+
The list of interferometer fits which are visualized.
129+
quick_update
130+
If ``True``, only the combined dirty-image subplot is written (no extra
131+
log-stretched variants), so this is safe to call from the search's
132+
quick-update hook.
133+
"""
134+
def should_plot(name):
135+
return plot_setting(section=["fit", "fit_interferometer"], name=name)
136+
137+
output_path = str(self.image_path)
138+
fmt = self.fmt
139+
140+
if should_plot("subplot_fit") or quick_update:
141+
subplot_fit_interferometer_combined(
142+
fit_list,
143+
output_path=output_path,
144+
output_format=fmt,
145+
title_prefix=self.title_prefix,
146+
)

autolens/interferometer/model/visualizer.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,56 @@ def visualize(
161161
)
162162
except IndexError:
163163
pass
164+
165+
@staticmethod
166+
def visualize_combined(
167+
analyses,
168+
paths: af.AbstractPaths,
169+
instance: af.ModelInstance,
170+
during_analysis: bool,
171+
quick_update: bool = False,
172+
):
173+
"""
174+
Performs visualization during the non-linear search of information that is
175+
shared across all per-channel interferometer analyses, on a single multi-row
176+
figure. Used for ALMA-style datacube fits where each channel is its own
177+
``Interferometer`` dataset wrapped in an ``af.AnalysisFactor``.
178+
179+
Outputs ``fit_combined.png``: a row-per-channel subplot showing dirty image,
180+
dirty model image, source-plane reconstruction and dirty normalised residual
181+
map. The plot makes it easy to see how an emission line's source-plane
182+
morphology shifts across the cube while the lens model stays fixed.
183+
184+
Parameters
185+
----------
186+
analyses
187+
The list of all per-channel ``AnalysisInterferometer`` objects.
188+
paths
189+
The paths object which manages where visualisation is written to.
190+
instance
191+
A ``Collection`` of per-factor model instances. Iterating it yields one
192+
``ModelInstance`` per channel, in the same order as ``analyses``.
193+
during_analysis
194+
``True`` when called during the non-linear search, ``False`` when
195+
called after the search completes.
196+
quick_update
197+
``True`` when called from the search's quick-update hook between
198+
iterations; only the headline combined plot is written in that case.
199+
"""
200+
201+
if analyses is None:
202+
return
203+
204+
plotter = PlotterInterferometer(
205+
image_path=paths.image_path, title_prefix=analyses[0].title_prefix
206+
)
207+
208+
fit_list = [
209+
analysis.fit_for_visualization(instance=single_instance)
210+
for analysis, single_instance in zip(analyses, instance)
211+
]
212+
213+
plotter.fit_interferometer_combined(
214+
fit_list=fit_list,
215+
quick_update=quick_update,
216+
)

autolens/interferometer/plot/fit_interferometer_plots.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,102 @@ def subplot_fit_dirty_images(
328328
save_figure(fig, path=output_path, filename="fit_dirty_images", format=output_format)
329329

330330

331+
def subplot_fit_interferometer_combined(
332+
fit_list,
333+
output_path: Optional[str] = None,
334+
output_format: str = None,
335+
colormap: Optional[str] = None,
336+
title_prefix: str = None,
337+
):
338+
"""
339+
Produce a combined multi-row subplot for a list of `FitInterferometer` objects.
340+
341+
Each row corresponds to one channel of a datacube (or one dataset of a multi-band
342+
interferometer fit) and contains four panels:
343+
344+
* Dirty Image (data)
345+
* Dirty Model Image (with critical curves)
346+
* Source Plane (reconstruction)
347+
* Dirty Normalised Residual Map
348+
349+
The layout mirrors :func:`subplot_fit_combined` for imaging — same purpose,
350+
different panel choice because interferometer fits are most informatively
351+
visualised in dirty-image space.
352+
353+
Parameters
354+
----------
355+
fit_list : list of FitInterferometer
356+
The interferometer fits to display. Each fit occupies one row of the figure.
357+
output_path : str, optional
358+
Directory in which to save the figure. If ``None`` the figure is not saved.
359+
output_format : str, optional
360+
Image format passed to :func:`~autoarray.plot.utils.save_figure`.
361+
colormap : str, optional
362+
Matplotlib colormap name applied to all image panels.
363+
title_prefix : str, optional
364+
Optional prefix prepended to every panel title.
365+
"""
366+
n_fits = len(fit_list)
367+
n_cols = 4
368+
fig, axes = subplots(n_fits, n_cols, figsize=conf_subplot_figsize(n_fits, n_cols))
369+
if n_fits == 1:
370+
all_axes = [list(axes)]
371+
else:
372+
all_axes = [list(axes[i]) for i in range(n_fits)]
373+
374+
final_plane_index = len(fit_list[0].tracer.planes) - 1
375+
376+
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
377+
for row, fit in enumerate(fit_list):
378+
row_axes = all_axes[row]
379+
380+
tracer = fit.tracer_linear_light_profiles_to_light_profiles
381+
cc_grid = fit.dataset.real_space_mask.derive_grid.all_false
382+
ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curve_lines(
383+
tracer, cc_grid
384+
)
385+
386+
plot_array(
387+
array=fit.dirty_image,
388+
ax=row_axes[0],
389+
title=_pf(f"Dirty Image (ch {row})"),
390+
colormap=colormap,
391+
)
392+
393+
plot_array(
394+
array=fit.dirty_model_image,
395+
ax=row_axes[1],
396+
title=_pf("Dirty Model Image"),
397+
colormap=colormap,
398+
lines=ip_lines,
399+
line_colors=ip_colors,
400+
)
401+
402+
try:
403+
_plot_source_plane(
404+
fit,
405+
row_axes[2],
406+
final_plane_index,
407+
colormap=colormap,
408+
title=_pf(f"Source Plane {final_plane_index}"),
409+
lines=sp_lines,
410+
line_colors=sp_colors,
411+
)
412+
except Exception:
413+
row_axes[2].axis("off")
414+
415+
plot_array(
416+
array=fit.dirty_normalized_residual_map,
417+
ax=row_axes[3],
418+
title=_pf("Dirty Norm Residual"),
419+
colormap=colormap,
420+
cb_unit=r"$\sigma$",
421+
)
422+
423+
tight_layout()
424+
save_figure(fig, path=output_path, filename="fit_combined", format=output_format)
425+
426+
331427
def subplot_fit_real_space(
332428
fit,
333429
output_path: Optional[str] = None,

0 commit comments

Comments
 (0)