diff --git a/src/km3irf/build_aeff.py b/src/km3irf/build_aeff.py index ee20c1c9b69309fb5fe17eb83492dc93887f20dd..8083e37061f679c24211e13b0262447004a160b8 100644 --- a/src/km3irf/build_aeff.py +++ b/src/km3irf/build_aeff.py @@ -7,7 +7,7 @@ import pandas as pd import uproot as ur from km3io import OfflineReader -from .irf_tools import aeff_2D +from .irf_tools import aeff_2D, psf_3D # import matplotlib.pyplot as plt # from matplotlib.colors import LogNorm @@ -17,6 +17,7 @@ import astropy.units as u from astropy.io import fits from scipy.stats import binned_statistic +from scipy.ndimage import gaussian_filter1d, gaussian_filter # from collections import defaultdict @@ -105,7 +106,6 @@ class DataContainer: theta_binC = np.arccos(0.5 * (cos_theta_binE[:-1] + cos_theta_binE[1:])) # Fill histograms for effective area - # !!! Check number of events uproot vs pandas aeff_all = ( aeff_2D( e_bins=energy_binE, @@ -125,6 +125,100 @@ class DataContainer: return None +def build_psf( + self, + df_pass, + cos_theta_binE=np.linspace(1, -1, 7), + energy_binE=np.logspace(2, 8, 25), + rad_binE=np.concatenate( + ( + np.linspace(0, 1, 21), + np.linspace(1, 5, 41)[1:], + np.linspace(5, 30, 51)[1:], + [180.0], + ) + ), + norm=False, + smooth=True, + smooth_norm=True, + output="psf.fits", +): + """ + Build Point Spread Function 3D .fits + + df_pass: incoming data frame + + cos_theta_binE: numpy array of linear bins for cos of zenith angle theta + + energy_binE: log numpy array of enegy bins + + rad_binE: numpy array oflinear radial bins + (20 bins for 0-1 deg, 40 bins for 1-5 deg, 50 bins for 5-30 deg, + 1 final bin up to 180 deg) + + output: name of generated PSF file with extension .fits + + """ + theta_binE = np.arccos(cos_theta_binE) + # Bin centers + energy_binC = np.sqrt(energy_binE[:-1] * energy_binE[1:]) + theta_binC = np.arccos(0.5 * (cos_theta_binE[:-1] + cos_theta_binE[1:])) + rad_binC = 0.5 * (rad_binE[1:] + rad_binE[:-1]) + + # Fill histogram for PSF + psf = psf_3D( + e_bins=energy_binE, + r_bins=rad_binE, + t_bins=theta_binE, + dataset=df_pass, + weights=1, + ) + + # compute dP/dOmega + sizes_rad_bins = np.diff(rad_binE**2) + norma = psf.sum(axis=0, keepdims=True) + psf /= sizes_rad_bins[:, None, None] * (np.pi / 180) ** 2 * np.pi + + # Normalization for PSF + if norm: + psf = np.nan_to_num(psf / norma) + + # Smearing + if smooth and not norm: + s1 = gaussian_filter1d(psf, 0.5, axis=0, mode="nearest") + s2 = gaussian_filter1d(psf, 2, axis=0, mode="nearest") + s3 = gaussian_filter1d(psf, 4, axis=0, mode="nearest") + s4 = gaussian_filter1d(psf, 6, axis=0, mode="constant") + psf = np.concatenate( + (s1[:10], s2[10:20], s3[20:60], s4[60:-1], [psf[-1]]), axis=0 + ) + # smooth edges between the different ranges + psf[10:-1] = gaussian_filter1d(psf[10:-1], 1, axis=0, mode="nearest") + if smooth_norm: + norm_psf_sm = ( + psf * sizes_rad_bins[:, None, None] * (np.pi / 180) ** 2 * np.pi + ).sum(axis=0, keepdims=True) + psf = np.nan_to_num(psf / norm_psf_sm) + elif smooth and norm: + raise Exception("smooth and norm cannot be True at the same time") + + new_psf_file = WritePSF( + energy_binC, + energy_binE, + theta_binC, + theta_binE, + rad_binC, + rad_binE, + psf_T=psf, + ) + new_psf_file.to_fits(file_name=output) + + return None + + +def build_edisp(): + pass + + def unpack_data(no_bdt, uproot_file): """ retrieve information from data and pack it to pandas DataFrame @@ -246,3 +340,89 @@ class WriteAeff: aeff_fits.writeto(file_name, overwrite=True) return print(f"file {file_name} is written successfully!") + + +# Class for writing PSF to fits files +class WritePSF: + def __init__( + self, + energy_binC, + energy_binE, + theta_binC, + theta_binE, + rad_binC, + rad_binE, + psf_T, + ): + self.col1 = fits.Column( + name="ENERG_LO", + format="{}E".format(len(energy_binC)), + unit="GeV", + array=[energy_binE[:-1]], + ) + self.col2 = fits.Column( + name="ENERG_HI", + format="{}E".format(len(energy_binC)), + unit="GeV", + array=[energy_binE[1:]], + ) + self.col3 = fits.Column( + name="THETA_LO", + format="{}E".format(len(theta_binC)), + unit="rad", + array=[theta_binE[:-1]], + ) + self.col4 = fits.Column( + name="THETA_HI", + format="{}E".format(len(theta_binC)), + unit="rad", + array=[theta_binE[1:]], + ) + self.col5 = fits.Column( + name="RAD_LO", + format="{}E".format(len(rad_binC)), + unit="deg", + array=[rad_binE[:-1]], + ) + self.col6 = fits.Column( + name="RAD_HI", + format="{}E".format(len(rad_binC)), + unit="deg", + array=[rad_binE[1:]], + ) + self.col7 = fits.Column( + name="RPSF", + format="{}D".format(len(energy_binC) * len(theta_binC) * len(rad_binC)), + dim="({},{},{})".format(len(energy_binC), len(theta_binC), len(rad_binC)), + unit="sr-1", + array=[psf_T], + ) + + def to_fits(self, file_name): + cols = fits.ColDefs( + [ + self.col1, + self.col2, + self.col3, + self.col4, + self.col5, + self.col6, + self.col7, + ] + ) + hdu = fits.PrimaryHDU() + hdu2 = fits.BinTableHDU.from_columns(cols) + hdu2.header["EXTNAME"] = "PSF_2D_TABLE" + hdu2.header[ + "HDUDOC" + ] = "https://github.com/open-gamma-ray-astro/gamma-astro-data-formats" + hdu2.header["HDUVERS"] = "0.2" + hdu2.header["HDUCLASS"] = "GADF" + hdu2.header["HDUCLAS1"] = "RESPONSE" + hdu2.header["HDUCLAS2"] = "RPSF" + hdu2.header["HDUCLAS3"] = "FULL-ENCLOSURE" + hdu2.header["HDUCLAS4"] = "PSF_TABLE" + psf_fits = fits.HDUList([hdu, hdu2]) + psf_fits.writeto(file_name, overwrite=True) + + return print(f"file {file_name} is written successfully!")