Example 5: NPTF Correction for the Point Spread Function (PSF)

In this example we show how to account for the PSF correction using psf_correction.py

Fundamentally the presence of a non-zero PSF implies that the photons from any point source will be smeared out into some region around its true location. This effect must be accounted for in the NPTF. This is achieved via a function \(\rho(f)\). In the code we discretize \(\rho(f)\) as an approximation to the full function.

The two outputs of an instance of psf_correction are: 1. f_ary, an array of f values; and 2. df_rho_div_f_ary, an associated array of \(\Delta f \rho(f)/f\) values, where \(\Delta f\) is the width of the f_ary bins.

If the angular reconstruction of the data is perfect, then \(\rho(f) = \delta(f-1)\). In many situations, such as for the Fermi data at higher energies, a Gaussian approximation of the PSF will suffice. Even then there are a number of variables that go into evaluating the correction, as shown below. Finally we will show how the code can be used for the case of non-Gaussian PSFs.

As the calculation of \(\rho(f)\) can be time consuming, we always save the output to avoid recomputing the same correction twice. Consequently it can be convenient to have a common psf_dir where all PSF corrections for the runs are stored.

Example 1: Default Gaussian PSF

We start by showing the PSF correction for a Gaussian PSF - that is the PSF as a function of \(r\) is \(\exp \left[ -r^2 / (2\sigma^2) \right]\) - with \(\sigma\) set to the value of the 68% containment radius for the PSF of the Fermi dataset we will use in later examples.

File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.181_10_50000_1000_0.01.npy
f_ary: [0.05 0.15 0.25 0.35 0.45 0.55 0.65 0.75 0.85 0.95]
df_rho_div_f_ary: [65.29502702  6.86435887  2.543033    1.28251213  0.80024078  0.54595125
  0.09246876  0.          0.          0.        ]
Text(0.5,1.04,'Gaussian PSF, $\sigma_\mathrm{PSF} = 0.1812$')
_images/Example5_PSF_Correction_5_2.png

Example 2: Impact of changing \(\sigma\)

Here we show the impact on the PSF of changing \(\sigma\). From the plot we can see that for a small PSF, \(\rho(f)\) approaches the no PSF case of \(\delta(f-1)\) implying that the flux fractions are concentrated at a single large value. As \(\sigma\) increases we move away from this idealized scenario and the flux becomes more spread out, leading to a \(\rho(f)\) peaked at lower flux values.

File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.05_10_50000_1000_0.01.npy
File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.4_10_50000_1000_0.01.npy
Text(0.5,1.04,'Varying $\sigma_\mathrm{PSF}$')
_images/Example5_PSF_Correction_8_2.png

Example 3: Changing the default options for determining \(\rho(f)\)

In this example we show how for a given PSF, the other parameters associated with how accurately we calculate \(\rho(f)\) can impact what we get back. The parameters that can be changed are are:

Argument Defaults Purpose
num_f_bins 10 number of f_bins used
n_psf 50000 number of PSFs placed down when calculating
n_pts_per_psf 1000 number of points to place per psf in the calculation
f_trunc 0.01 minimum flux fraction to keep track of
nside 128 nside of the map the PSF is used on

The default parameters have been chosen to be accurate enough for the Fermi analyses we will be performed later. But if the user changes the PSF (even just \(\sigma\)), it is important to be sure that the above parameters are chosen so that \(\rho(f)\) is evaluated accurately enough.

In general increasing num_f_bins, n_psf, and n_pts_per_psf, whilst decreasing f_trunc leads to a more accurate \(\rho(f)\). But each will also slow down the evaluation of \(\rho(f)\), and in the case of num_f_bin, slow down the subsequent non-Poissonian likelihood evaluation.

nside should be set to the value of the map being analysed, but we also highlight the impact of changing it below. For an analysis on a non-HEALPix grid, the PSF can often be approximated by an appropriate HEALPix binning. If this is not the case, however, a different approach must be pursued in calculating \(\rho(f)\).

File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.181_20_50000_1000_0.01.npy
File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.181_10_5000_100_0.01.npy
File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.181_10_50000_1000_0.1.npy
File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_64_0.181_10_50000_1000_0.01.npy
<matplotlib.legend.Legend at 0x7f393e0dc470>
_images/Example5_PSF_Correction_11_2.png

Example 4: PSF on a Cartesian Grid

For some applications, particularly when analyzing smaller regions of the sky, it may be desirable to work with data on a Cartesian grid rather than a healpix map. Note generally for larger regions, in order to account for curvature on the sky a healpix pixelization is recommended. Code to convert from Cartesian grids to healpix can be found here: https://github.com/nickrodd/grid2healpix

In order to calculate the appropriate PSF correction for Cartesian maps the general syntax is the same, except now the healpix_map keyword should be set to False and the pixarea keyword set to the area in sr of each pixel of the Cartesian map. In addition the gridsize keyword determines how large the map is, and flux that falls outside the map is lost in the Cartesian case.

As an example of this syntax we calculate the PSF correction on a Cartesian map that has pixels the same size as an nside=128 healpix map, and compare the two PSF corrections. Note they are essentially identical.

File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_0.21_0.181_10_50000_1000_0.01.npy
Text(0,0.5,'$f \times \rho(f)$')
_images/Example5_PSF_Correction_14_2.png

Example 5: Custom PSF

In addition to the default Gausian PSF, psf_correction.py also has the option of taking in a custom PSF. In order to use this ability, the user needs to initialise psf_correction with delay_compute=True, manually define the parameters that define the PSF and then call make_or_load_psf_corr.

The variables that need to be redefined in the instance of psf_correction are:

Argument Purpose
psf_r_func the psf as a function of r, distance in radians from the center of the point source
sample_psf_m ax maximum distance to sample the psf from the center, should be larger for psfs with long tails
``psf_samples` ` number of samples to make from the psf (linearly spaced) from 0 to sample_psf_m ax, should be large enough to adequately represent the full psf
psf_tag label the psf is saved with

As an example of a more complicated PSF we consider the full Fermi-LAT PSF. The PSF of Fermi is approximately Gaussian near the core, but has larger tails. To model this a pair of King functions are used to describe the radial distribution. Below we show a comparison between the Gaussian approximation and the full PSF, for two different energies. As shown, for low energies where the Fermi PSF is larger, the difference between the two can be significant. For higher energies where the PSF becomes smaller, however, the difference is marginal.

For the full details of the Fermi-LAT PSF, see: http://fermi.gsfc.nasa.gov/ssc/data/analysis/documentation/Cicerone/Cicerone_LAT_IRFs/IRF_PSF.html

File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.235_10_50000_1000_0.01.npy
File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/Fermi_PSF_2GeV.npy
<matplotlib.legend.Legend at 0x7f393e00dd30>
_images/Example5_PSF_Correction_17_2.png
File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/gauss_128_0.055_10_50000_1000_0.01.npy
File saved as: /zfs/nrodd/CodeDev/RerunNPTFExDiffFix/psf_dir/Fermi_PSF_20GeV.npy
<matplotlib.legend.Legend at 0x7f393df7a8d0>
_images/Example5_PSF_Correction_18_2.png

The above example also serves as a tutorial on how to combine various PSFs into a single PSF. In the case of the Fermi PSF the full radial dependence is the sum of two King functions. More generally if the full PSF is a combination of multiple individual ones (for example from multiple energy bins), then this can be formed by just adding these functions with an appropriate weighting to get a single psf_r_func.