Source code for tools21cm.primary_beam

'''
Methods to simulate the primary beam of radio telescope
'''

import numpy as np
from tqdm import tqdm
from scipy.special import j1
from astropy import units

from . import cosmo as cm
from . import conv

[docs] def primary_beam(array, z, nu_axis=2, beam_func='Gaussian', boxsize=None, D_station=40., l_null=None, verbose=True): """ array : ndarray The array of brightness temperature. z : float redshift. With one value, the function will assume the array to be coeval. In case of light-cone, provide size of z should be equivalent to the length of frequency axis of the array. nu_axis : int The frequency axis of the array (Default: 2) beam_func: str The type of function to model the primary beam. The options are 'gaussian', 'bessel', 'sigmoid' and 'step'. Default: 'gaussian' boxsize : float Size of the box in physical units (cMpc). Default: From set simulation constants. D_station: float Diameter of the station in metres. Default: 40. l_null: float Length (in Mpc) of the first null. Default: None, which will use the diameter of the station to estimate this value. """ assert array.ndim > 1 if boxsize is None: boxsize = conv.LB beam = np.zeros(array.shape) if array.ndim==2: beamed = array*circular_beam(array.shape[0], z, D_station=D_station, beam_func=beam_func, boxsize=boxsize) else: if nu_axis!=2: array = np.swapaxes(array, nu_axis, 2) if np.array(z).size==1: z = z*np.ones(array.shape[2]) for i in tqdm(range(z.size), disable=not verbose): beam[:,:,i] = circular_beam(array.shape[0], z[i], D_station=D_station, beam_func=beam_func, boxsize=boxsize, l_null=l_null) beamed = array*beam if nu_axis!=2: beamed = np.swapaxes(beamed, 2, nu_axis) return beamed
[docs] def primary_beam_null(z, D_station=40.): """ Calculates the physical diameter of the first null of an Airy disk projected on the sky at redshift z. """ if isinstance(D_station,units.Quantity): D_station = D_station.to('m').value l_null = cm.z_to_cdist(z)*cm.nu_to_wavel(cm.z_to_nu(z))/D_station*1.22 return l_null
[docs] def circular_beam(ncells, z, D_station=40., beam_func='Gaussian', boxsize=None, l_null=None): """ Generates a 2D circular beam pattern. """ if boxsize is None: boxsize = conv.LB # Get the diameter of the first null in cMpc if l_null is None: l_null = primary_beam_null(z, D_station=D_station) # Create a grid of distances from the center xx, yy = np.mgrid[-ncells/2:ncells/2,-ncells/2:ncells/2]*boxsize/ncells rr = np.sqrt(xx**2+yy**2) if beam_func.lower()=='step': beam = np.zeros_like(rr) # Use the radius of the first null (l_null / 2) beam[rr<=(l_null/2)] = 1 elif beam_func.lower()=='gaussian': # The FWHM of an Airy disk is ~1.029 * lambda / D, which is l_null / 1.186 fwhm = l_null / 1.186 sigma = fwhm / 2.355 beam = np.exp(-rr**2 / (2. * sigma**2)) elif beam_func.lower() == 'bessel': r_null = l_null # The variable l_null is the radius of the first null. # Calculate the scaling factor alpha for the Airy disk formula alpha = 3.8317 / r_null # The first zero of J1(x) is at x approx 3.8317 argument = alpha * rr # The argument for the Bessel function # Handle the center (r=0) to avoid division by zero. # The limit of (2*J1(x)/x)^2 as x->0 is 1.0. center_idx = np.where(rr == 0) argument[center_idx] = 1e-9 # Use a small number to prevent error # Calculate the beam pattern using the Airy disk formula beam = (2 * j1(argument) / argument)**2 beam[center_idx] = 1.0 # Manually set the peak to 1 elif beam_func.lower()=='sigmoid': r0, b = l_null/2., 0.5 beam = 1.-1/(1+np.exp(b*r0-b*rr**2**0.5)) else: raise ValueError("Please choose between 'step', 'gaussian', 'bessel', and 'sigmoid' for beam_func.") return beam/beam.max()