Effective Beam Response

This notebook demonstrates how to estimate the effective beam response of an IViS reconstruction, which varies with the strength of the regularization (e.g., Laplacian filtering).

Download

You can download the data directory data_tutorials using:

wget https://www.mso.anu.edu.au/~amarchal/shared/ivis/data_tutorials.zip
unzip data_tutorials.zip -d data_tutorials
rm data_tutorials.zip

Kindly provided by Enrico Di Teodoro (UniFI) and Karlie Noon (RSAA/ANU).

Method

We inject a delta function (point source) into an otherwise empty map and simulate visibilities using a realistic uv-coverage from a single MeerKAT beam.

We then reconstruct the image using IViS to estimate the response.

This response differs from a traditional “restored” beam, as it results from a non-linear reconstruction that depends on the intrinsic signal shape, signal-to-noise ratio, and the strength of spatial regularization.

While this beam characterizes IViS’s response to a point source for a given uv-sampling, the effective resolution for extended emission may differ due to the nonlinearity of the reconstruction.

We therefore caution against interpreting this beam as a global PSF of the reconstruction. Nonetheless, it provides a useful starting point for estimating the shape and size of a theoretical effective beam.

The resulting image can be reconvolved with a known kernel, depending on the needs of the scientific application.

Feel free to vary the scaling for the noise, or the lambda_r, the hyper-parameter controling the Laplacian filtering, to see how the response varies.

import glob
from tqdm import tqdm as tqdm

import numpy as np
import matplotlib.pyplot as plt
from astropy.io import fits
from radio_beam import Beam
from astropy import units as u
from reproject import reproject_interp

from ivis.io import DataProcessor
from ivis.imager import Imager3D
from ivis.logger import logger
from ivis.utils import dutils
from ivis.readers import CasacoreReader
from ivis.types import VisIData

from ivis.models import ClassicIViS3D

path_ms = "./data_tutorials/ivis_data_1beam/msl_mw/" #directory of measurement sets    
path_beams = "./data_tutorials/ivis_data_1beam/BEAMS/" #directory of primary beams
path_sd = None #path single-dish data
pathout = "./data_tutorials/ivis_data_1beam/" #path where data will be packaged and stored

#REF WCS INPUT USER
filename = "./data_tutorials/ivis_data_1beam/MW-C10_mom0th_NHI.fits"
target_header = fits.open(filename)[0].header
shape = (target_header["NAXIS2"],target_header["NAXIS1"])
    
#create data processor
data_processor = DataProcessor(path_ms, path_beams, path_sd, pathout)

# pre-compute pb and interpolation grids — this can be commented after first compute
logger.disabled = True
data_processor.compute_pb_and_grid(target_header, fitsname_pb="reproj_pb.fits", fitsname_grid="grid_interp.fits") 
logger.disabled = False

pb, grid = data_processor.read_pb_and_grid(fitsname_pb="reproj_pb.fits", fitsname_grid="grid_interp.fits")

#Dummy sd array
sd = np.zeros(shape)
#Dummy Beam sd
beam_sd = Beam(1*u.deg, 1*u.deg, 1.e-12*u.deg)
[2025-08-25 07:26:05 UTC] [IViS] [info] [Initialize DataProcessor ]
[2025-08-25 07:26:05 UTC] [IViS] [info] -------------------------------------------------------------------------
[2025-08-25 07:26:05 UTC] [IViS] [info]  Timestamp: 2025-08-25 07:26:05 UTC
[2025-08-25 07:26:05 UTC] [IViS] [info] 
[2025-08-25 07:26:05 UTC] [IViS] [info]         _ _| \ \     / _)   ___| 
[2025-08-25 07:26:05 UTC] [IViS] [info]           |   \ \   /   | \___ \ 
[2025-08-25 07:26:05 UTC] [IViS] [info]           |    \ \ /    |       |
[2025-08-25 07:26:05 UTC] [IViS] [info]         ___|    \_/    _| _____/ 
[2025-08-25 07:26:05 UTC] [IViS] [info]         
[2025-08-25 07:26:05 UTC] [IViS] [info]  Version 1.0.0
[2025-08-25 07:26:05 UTC] [IViS] [info]  IViS is released as open-source software
[2025-08-25 07:26:05 UTC] [IViS] [info]  Author: @amarchal
[2025-08-25 07:26:05 UTC] [IViS] [info]  Documentation: https://ivis-dev.readthedocs.io/en/latest/
[2025-08-25 07:26:05 UTC] [IViS] [info]  Github: https://github.com/antoinemarchal/ivis
[2025-08-25 07:26:05 UTC] [IViS] [info] -------------------------------------------------------------------------
100%|█████████████████████████████████████████████| 1/1 [00:00<00:00,  2.03it/s]
# -------------------
# Read visibilities into VisIData dataclass
# -------------------
reader = CasacoreReader(
    prefer_weight_spectrum=False,
    keep_autocorr=False,
    n_workers=4)

I: VisIData = reader.read_blocks_I(
    path_ms,
    uvmin=0.0,
    uvmax=np.inf,
    chan_sel=slice(0, 1),
)
[2025-08-25 07:26:06 UTC] [IViS] [info] [BLOCK] Loading block from: ./data_tutorials/ivis_data_1beam/msl_mw/
[2025-08-25 07:26:06 UTC] [IViS] [info] [BLOCK] Loading 1 beam(s) from: ./data_tutorials/ivis_data_1beam/msl_mw/
[2025-08-25 07:26:06 UTC] [IViS] [info] [BLOCK] Parallel read with 4 workers (order-preserving; selective)
[2025-08-25 07:26:06 UTC] [IViS] [info]     [MS] Opening: ./data_tutorials/ivis_data_1beam/msl_mw/MW-C10_2_MW_chan_-32kms.ms
[2025-08-25 07:26:06 UTC] [IViS] [info]     [MS] Done: MW-C10_2_MW_chan_-32kms.ms  rows=1068720
[2025-08-25 07:26:06 UTC] [IViS] [info] [BLOCK] Done: nchan=1, nbeam=1, nvis_max=1068720 (read 1/1 beams)

Injecting a 1 Jy point source at the center

cell_size = (np.abs(target_header["CDELT2"]) *u.deg).to(u.arcsec).value
sky_model = np.zeros((1,shape[0],shape[1]), dtype=np.float32)
sky_model[0][shape[0] // 2, shape[1] // 2] = 1.0 / (cell_size ** 2)  # Jy / arcsec^2

Simulated visibilities (using IViS forward model)

#Model visibilities with IVis forward single frequency model
image_processor = Imager3D(I,      # visibilities
                         pb,            # array of primary beams
                         grid,          # array of interpolation grids
                         None,            # single dish data in unit of Jy/arcsec^2
                         None,       # beam of single-dish data in radio_beam format
                         target_header, # header on which to image the data
                         sky_model,   # init array of parameters
                         0,       # maximum number of iterations
                         0,     # hyper-parameter single-dish
                         False,    # impose a positivity constaint
                         0,        # device: 0 is GPU; "cpu" is CPU
                         0,
                         beam_workers=1)

model = ClassicIViS3D()
model_vis =  image_processor.forward_model(model=model)
[2025-08-25 07:26:06 UTC] [IViS] [info] CUDA unavailable or invalid index; using CPU.
[2025-08-25 07:26:06 UTC] [IViS] [info] CUDA unavailable or invalid index; using CPU.
[2025-08-25 07:26:06 UTC] [IViS] [info] [Initialize Imager3D       ]
[2025-08-25 07:26:06 UTC] [IViS] [info] Number of iterations to be performed by the optimizer: 0
[2025-08-25 07:26:06 UTC] [IViS] [warning] lambda_sd = 0 — No short-spacing correction.
[2025-08-25 07:26:06 UTC] [IViS] [info] Optimizer not bounded - Positivity == False

Adding realistic noise from the MeerKAT data beam

#Add noise
fact=1 #Scale the noise with this if needed
noise_real = np.random.normal(loc=0.0, scale=I.sigma_I*fact)
noise_imag = np.random.normal(loc=0.0, scale=I.sigma_I*fact)
noise = noise_real + 1j * noise_imag

# Add realistic thermal noise to the model visibilities
I.data_I = model_vis + noise

Imaging using IViS

#user parameters
max_its = 20
lambda_sd = 0 #not relevant here
lambda_r = 1 #Control the strength of the Laplacian filtering
cost_device = 0#"cpu" #0 is GPU and "cpu" is CPU
optim_device = 0
positivity = True #Set to true because we know here the sky is positive for a point source

#Initial parameters (zero array)
init_params = np.zeros((1,shape[0],shape[1]), dtype=np.float32)

#create image processor
image_processor = Imager3D(I,      # visibilities
                         pb,            # array of primary beams
                         grid,          # array of interpolation grids
                         sd,            # single dish data in unit of Jy/arcsec^2
                         beam_sd,       # beam of single-dish data in radio_beam format
                         target_header, # header on which to image the data
                         init_params,   # init array of parameters
                         max_its,       # maximum number of iterations
                         lambda_sd,     # hyper-parameter single-dish
                         positivity,    # impose a positivity constaint
                         cost_device,        # device: 0 is GPU; "cpu" is CPU
                         optim_device,
                         beam_workers=1)
#get image
model = ClassicIViS3D(lambda_r=lambda_r)
result = image_processor.process(model=model, units="Jy/arcsec^2") #"Jy/arcsec^2" or "K"
[2025-08-25 07:26:06 UTC] [IViS] [info] CUDA unavailable or invalid index; using CPU.
[2025-08-25 07:26:06 UTC] [IViS] [info] CUDA unavailable or invalid index; using CPU.
[2025-08-25 07:26:06 UTC] [IViS] [info] [Initialize Imager3D       ]
[2025-08-25 07:26:06 UTC] [IViS] [info] Number of iterations to be performed by the optimizer: 20
[2025-08-25 07:26:06 UTC] [IViS] [warning] lambda_sd = 0 — No short-spacing correction.
[2025-08-25 07:26:06 UTC] [IViS] [info] Optimizer bounded - Positivity == True
[2025-08-25 07:26:06 UTC] [IViS] [warning] Optimizer bounded - Because there is noise in the data, it is generally not recommanded to add a positivity constaint.
[2025-08-25 07:26:06 UTC] [IViS] [info] Units of output: Jy/arcsec^2.
[2025-08-25 07:26:08 UTC] [IViS] [info] Starting optimisation: SciPy L-BFGS-B (CPU optimizer), cost on cpu
[2025-08-25 07:26:10 UTC] [IViS] [info] [PID 58115] Total cost: 1.42683e+06
[2025-08-25 07:26:14 UTC] [IViS] [info] [PID 58115] Total cost: 1.39159e+10
[2025-08-25 07:26:15 UTC] [IViS] [info] [PID 58115] Total cost: 1.27046e+06
[2025-08-25 07:26:16 UTC] [IViS] [info] [PID 58115] Total cost: 1.23309e+06
[2025-08-25 07:26:17 UTC] [IViS] [info] [PID 58115] Total cost: 1.19012e+06
[2025-08-25 07:26:18 UTC] [IViS] [info] [PID 58115] Total cost: 1.1678e+06
[2025-08-25 07:26:19 UTC] [IViS] [info] [PID 58115] Total cost: 1.15663e+06
[2025-08-25 07:26:20 UTC] [IViS] [info] [PID 58115] Total cost: 1.15294e+06
[2025-08-25 07:26:21 UTC] [IViS] [info] [PID 58115] Total cost: 1.14810e+06
[2025-08-25 07:26:23 UTC] [IViS] [info] [PID 58115] Total cost: 1.14598e+06
[2025-08-25 07:26:24 UTC] [IViS] [info] [PID 58115] Total cost: 1.14429e+06
[2025-08-25 07:26:26 UTC] [IViS] [info] [PID 58115] Total cost: 1.14343e+06
[2025-08-25 07:26:27 UTC] [IViS] [info] [PID 58115] Total cost: 1.14208e+06
[2025-08-25 07:26:28 UTC] [IViS] [info] [PID 58115] Total cost: 1.14135e+06
[2025-08-25 07:26:29 UTC] [IViS] [info] [PID 58115] Total cost: 1.14047e+06
[2025-08-25 07:26:30 UTC] [IViS] [info] [PID 58115] Total cost: 1.13971e+06
[2025-08-25 07:26:31 UTC] [IViS] [info] [PID 58115] Total cost: 1.1392e+06
[2025-08-25 07:26:32 UTC] [IViS] [info] [PID 58115] Total cost: 1.1386e+06
[2025-08-25 07:26:34 UTC] [IViS] [info] [PID 58115] Total cost: 1.13787e+06
[2025-08-25 07:26:35 UTC] [IViS] [info] [PID 58115] Total cost: 1.13760e+06
[2025-08-25 07:26:37 UTC] [IViS] [info] [PID 58115] Total cost: 1.13717e+06
[2025-08-25 07:26:38 UTC] [IViS] [info] [PID 58115] Total cost: 1.13703e+06
[2025-08-25 07:26:39 UTC] [IViS] [warning] If you are using ASKAP's convention I = XX + YY (with no 1/2 factor) then multiply the output by 2. Here assuming I = 1/2 (XX + YY).
[2025-08-25 07:26:39 UTC] [IViS] [info] Successful run. Please clap.

Zoom in / cutout at the center of the image

#Cutout
ny, nx = shape
cy, cx = ny // 2, nx // 2  # center coordinates

zoom = 20  # pixels around center
cutout = result[0][cy - zoom:cy + zoom + 1, cx - zoom:cx + zoom + 1]

Fitting an elliptical Gaussian model

flux, Bmaj, Bmin, theta = dutils.fit_elliptical_gaussian(cutout, pixel_scale_arcsec=cell_size)
../_images/04ce9da4948166c3f56b3eda9adea16d5128393d15c6db625eb7576e70f9be83.png