Skip to content

Commit 9463c8c

Browse files
Jammy2211claude
authored andcommitted
Add subplot_fit_quick for interferometer quick updates
6-panel (2x3) fast-render subplot for interferometer data: Top row: Dirty Image, Dirty Model Image, Dirty Normalized Residual Bottom row: Vis Norm Residual (Real) scatter, Vis Norm Residual (Imag) scatter, Source Plane / Reconstruction Uses raw imshow + scatter on pre-converted numpy arrays (same approach as imaging subplot_fit_quick). Visibility scatter panels use rasterized rendering for speed with large UV datasets. Quick updates now call subplot_fit_quick instead of the heavier subplot_fit_dirty_images. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent dbee823 commit 9463c8c

2 files changed

Lines changed: 153 additions & 4 deletions

File tree

autolens/interferometer/model/plotter.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from autolens.interferometer.fit_interferometer import FitInterferometer
1313
from autolens.interferometer.plot.fit_interferometer_plots import (
1414
subplot_fit,
15+
subplot_fit_quick,
1516
subplot_fit_dirty_images,
1617
subplot_fit_interferometer_combined,
1718
subplot_fit_real_space,
@@ -85,16 +86,20 @@ def should_plot(name):
8586
title_prefix=self.title_prefix,
8687
)
8788

88-
if should_plot("subplot_fit_dirty_images") or quick_update:
89+
if quick_update:
90+
subplot_fit_quick(
91+
fit, output_path=output_path, output_format=fmt,
92+
title_prefix=self.title_prefix,
93+
)
94+
return
95+
96+
if should_plot("subplot_fit_dirty_images"):
8997
subplot_fit_dirty_images(
9098
fit, output_path=output_path, output_format=fmt,
9199
image_plane_lines=ip_lines, image_plane_line_colors=ip_colors,
92100
title_prefix=self.title_prefix,
93101
)
94102

95-
if quick_update:
96-
return
97-
98103
if should_plot("subplot_fit_real_space"):
99104
subplot_fit_real_space(
100105
fit, output_path=output_path, output_format=fmt,

autolens/interferometer/plot/fit_interferometer_plots.py

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

330330

331+
def _to_native_np_interf(array):
332+
"""Convert an autoarray Array2D to a plain numpy 2D array."""
333+
try:
334+
mask = array.mask
335+
slim = np.asarray(array.array)
336+
native = np.zeros(mask.shape_native)
337+
native[~np.asarray(mask)] = slim
338+
return native
339+
except AttributeError:
340+
arr = np.asarray(array)
341+
return arr if arr.ndim == 2 else arr
342+
343+
344+
def _quick_imshow_interf(ax, array_2d, title, extent, cmap, vmin=None, vmax=None):
345+
"""Minimal imshow for quick-update panels."""
346+
if array_2d is None:
347+
ax.axis("off")
348+
return
349+
ax.imshow(
350+
array_2d, cmap=cmap, vmin=vmin, vmax=vmax,
351+
extent=extent, aspect="auto", origin="lower",
352+
)
353+
ax.set_title(title, fontsize=8)
354+
ax.set_xticks([])
355+
ax.set_yticks([])
356+
357+
358+
def subplot_fit_quick(
359+
fit,
360+
output_path: Optional[str] = None,
361+
output_format: str = None,
362+
colormap: Optional[str] = None,
363+
title_prefix: str = None,
364+
):
365+
"""
366+
Produce a 6-panel quick-update subplot for an interferometer fit.
367+
368+
Arranges the following panels in a 2 × 3 grid:
369+
370+
* Dirty Image (data)
371+
* Dirty Model Image
372+
* Dirty Normalised Residual Map
373+
* Visibility Normalised Residual (Real) vs UV distance
374+
* Visibility Normalised Residual (Imag) vs UV distance
375+
* Source plane image / reconstruction
376+
377+
Uses raw ``imshow`` and ``scatter`` calls on pre-converted numpy
378+
arrays for sub-second rendering.
379+
"""
380+
import matplotlib.pyplot as plt
381+
382+
# Pre-convert dirty images to numpy 2D (each is an inverse FFT)
383+
dirty_image = _to_native_np_interf(fit.dirty_image)
384+
dirty_model = _to_native_np_interf(fit.dirty_model_image)
385+
dirty_norm_resid = _to_native_np_interf(fit.dirty_normalized_residual_map)
386+
387+
extent = fit.dataset.real_space_mask.geometry.extent
388+
389+
if colormap is None:
390+
try:
391+
from autoarray.plot.utils import _default_colormap
392+
colormap = _default_colormap()
393+
except Exception:
394+
colormap = "default"
395+
396+
_pf = (lambda t: f"{title_prefix.rstrip()} {t}") if title_prefix else (lambda t: t)
397+
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
398+
axes_flat = list(axes.flatten())
399+
400+
# Top row: Dirty Image, Dirty Model Image, Dirty Normalized Residual
401+
_quick_imshow_interf(axes_flat[0], dirty_image, _pf("Dirty Image"), extent, colormap)
402+
_quick_imshow_interf(axes_flat[1], dirty_model, _pf("Dirty Model Image"), extent, colormap)
403+
404+
finite = dirty_norm_resid[np.isfinite(dirty_norm_resid)]
405+
abs_max = float(np.max(np.abs(finite))) if len(finite) > 0 else 1.0
406+
_quick_imshow_interf(
407+
axes_flat[2], dirty_norm_resid, _pf("Dirty Norm Residual"),
408+
extent, colormap, vmin=-abs_max, vmax=abs_max,
409+
)
410+
411+
# Bottom row: Visibility residuals (Real/Imag scatter) + Source Plane
412+
norm_resid_vis = np.asarray(fit.normalized_residual_map)
413+
uv_dist = np.asarray(fit.dataset.uv_distances) / 1e3
414+
415+
ax_real = axes_flat[3]
416+
ax_real.scatter(uv_dist, np.real(norm_resid_vis), s=0.5, alpha=0.3, c="k", rasterized=True)
417+
ax_real.set_title(_pf("Vis Norm Resid (Real)"), fontsize=8)
418+
ax_real.set_xlabel("UV dist (kλ)", fontsize=7)
419+
ax_real.set_ylabel("σ", fontsize=7)
420+
ax_real.tick_params(labelsize=6)
421+
422+
ax_imag = axes_flat[4]
423+
ax_imag.scatter(uv_dist, np.imag(norm_resid_vis), s=0.5, alpha=0.3, c="k", rasterized=True)
424+
ax_imag.set_title(_pf("Vis Norm Resid (Imag)"), fontsize=8)
425+
ax_imag.set_xlabel("UV dist (kλ)", fontsize=7)
426+
ax_imag.set_ylabel("σ", fontsize=7)
427+
ax_imag.tick_params(labelsize=6)
428+
429+
# Source plane: parametric → small grid, pixelized → plot_mapper
430+
tracer_viz = fit.tracer_linear_light_profiles_to_light_profiles
431+
final_plane_index = len(tracer_viz.planes) - 1
432+
source_galaxies = tracer_viz.planes[final_plane_index]
433+
has_pixelization = any(
434+
hasattr(g, "pixelization") and g.pixelization is not None
435+
for g in source_galaxies
436+
)
437+
438+
if not has_pixelization:
439+
try:
440+
rs_mask = fit.dataset.real_space_mask
441+
quick_grid = aa.Grid2D.uniform(
442+
shape_native=(50, 50),
443+
pixel_scales=rs_mask.pixel_scales,
444+
origin=rs_mask.origin,
445+
)
446+
source_img = plane_image_from(
447+
galaxies=source_galaxies, grid=quick_grid,
448+
zoom_to_brightest=False,
449+
)
450+
src_np = _to_native_np_interf(source_img)
451+
_quick_imshow_interf(
452+
axes_flat[5], src_np, _pf("Source Plane"),
453+
quick_grid.geometry.extent, colormap,
454+
)
455+
except Exception:
456+
axes_flat[5].axis("off")
457+
else:
458+
try:
459+
inversion = fit.inversion
460+
mapper_list = inversion.cls_list_from(cls=Mapper)
461+
mapper = mapper_list[final_plane_index - 1] if final_plane_index > 0 else mapper_list[0]
462+
pixel_values = inversion.reconstruction_dict[mapper]
463+
plot_mapper(
464+
mapper, solution_vector=pixel_values, ax=axes_flat[5],
465+
title=_pf("Source Reconstruction"), colormap=colormap,
466+
zoom_to_brightest=False,
467+
)
468+
except Exception:
469+
axes_flat[5].axis("off")
470+
471+
fig.tight_layout(pad=0.5)
472+
save_figure(fig, path=output_path, filename="fit_quick", format=output_format, dpi=100)
473+
474+
331475
def subplot_fit_interferometer_combined(
332476
fit_list,
333477
output_path: Optional[str] = None,

0 commit comments

Comments
 (0)