Source code for gammapy.irf.psf_core

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""Utility functions for reading / writing PSF parameters to JSON files.

From the documentaion of load_psf():

The PSF model is convolved with the source model when
set_psf is issued,and the PSF-convolved model is then fit
to data set  when the fit command is run.

Note: it is important that you set xpos and ypos
of your PSF such that it is fully contained in the
current dataset.
Otherwise your PSF will be cut off and your results nonsense!
You can simply set the PSF in the middle of your image
by issueing center_psf()
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import json
import numpy as np
from ..extern import six
from astropy.convolution import Gaussian2DKernel
from astropy.stats import gaussian_fwhm_to_sigma, gaussian_sigma_to_fwhm
from ..image.models import Gauss2DPDF, MultiGauss2D

__all__ = [
    'GaussPSF',
    'HESSMultiGaussPSF',
    'SherpaMultiGaussPSF',
    'multi_gauss_psf_kernel',
]


[docs]class GaussPSF(Gauss2DPDF): """Extension of Gauss2D PDF by PSF-specific functionality."""
[docs] def to_hess(self): return {'A_1': self.norm, 'sigma_1': self.sigma}
[docs] def to_sherpa(self, binsz): """Generate gauss2d parameters for Sherpa such that the integral is 1""" d = {} d['ampl'] = binsz ** 2 * self.norm d['fwhm'] = gaussian_sigma_to_fwhm / binsz * self.sigma return {'psf1': d}
[docs]class SherpaMultiGaussPSF(object): """Multi-Gauss PSF as represented in the Sherpa software. Note that Sherpa uses the following function f(x,y) = f(r) = A exp[-f(r/F)^2] f = 2.7725887 = 4log2 relates the full-width at half-maximum F to the Gaussian sigma """ def __init__(self, source): if isinstance(source, dict): # Assume source is a dict with correct format self.pars = source # elif isinstance(source, HESS): # Get pars dict by from HESS object # self.pars = source.to_sherpa() elif isinstance(source, six.string_types): # Assume it is a JSON filename fh = open(source) self.pars = json.load(fh) fh.close() else: raise ValueError('Unknown source: {}'.format(source)) def __str__(self): return json.dumps(self.pars, sort_keys=True, indent=4)
[docs] def center_psf(self): """Set ``xpos`` and ``ypos`` of the PSF to the dataspace center.""" import sherpa.astro.ui as sau try: ny, nx = sau.get_data().shape for _ in ['psf1', 'psf2', 'psf3']: par = sau.get_par(_ + '.xpos') par.val = nx / 2. par = sau.get_par(_ + '.ypos') par.val = ny / 2. except: raise Exception('PSF is not centered.')
[docs] def set(self): """Set the PSF for Sherpa.""" import sherpa.astro.ui as sau from ._utils_old import read_json read_json(self.pars, sau.set_model) sau.load_psf('psf', sau.get_model()) self.center_psf() sau.set_psf('psf')
[docs] def evaluate(self, t, ampl1, fwhm1, ampl2, fwhm2, ampl3, fwhm3): """Hand-coded evaluate for debugging.""" f = 4 * np.log(2) psf1 = ampl1 * np.exp(-f * t ** 2 / fwhm1 ** 2) psf2 = ampl2 * np.exp(-f * t ** 2 / fwhm2 ** 2) psf3 = ampl3 * np.exp(-f * t ** 2 / fwhm3 ** 2) return psf1 + psf2 + psf3
[docs] def containment_fraction(self, theta, npix=1000): """Compute fraction of PSF contained inside theta.""" import sherpa.astro.ui as sau sau.dataspace2d((npix, npix)) self.set() # x_center = get_psf().kernel.pars.xpos # y_center = get_psf().kernel.pars.ypos x_center, y_center = sau.get_psf().model.center x_center, y_center = x_center + 0.5, y_center + 0.5 # shift seen on image. x, y = sau.get_data().x0, sau.get_data().x1 # Note: Here we have to use the source image, before I used # get_model_image(), which returns the PSF-convolved PSF image, # which is a factor of sqrt(2) ~ 1.4 too wide!!! p = sau.get_source_image().y.flatten() p /= np.nansum(p) mask = (x - x_center) ** 2 + (y - y_center) ** 2 < theta ** 2 fraction = np.nansum(p[mask]) if 0: # debug sau.get_data().y = p sau.save_data('psf_sherpa.fits', clobber=True) sau.get_data().y = mask.astype('int') sau.save_data('mask_sherpa.fits', clobber=True) return fraction
[docs]class HESSMultiGaussPSF(object): """Multi-Gauss PSF as represented in the HESS software. The 2D Gaussian is represented as a 1D exponential probability density function per offset angle squared: dp / dtheta**2 = [0]*(exp(-x/(2*[1]*[1]))+[2]*exp(-x/(2*[3]*[3])) @param source: either a dict of a filename The following two parameters control numerical precision / speed. Usually the defaults are fine. @param theta_max: Maximum offset in numerical computations @param npoints: Number of points in numerical computations @param eps: Allowed tolerance on normalization of total P to 1 """ def __init__(self, source): if isinstance(source, dict): # Assume source is a dict with correct format self.pars = source else: # Assume source is a filename with correct format self.pars = self._read_ascii(source) # Scale will be computed from normalization anyways, # so any default is fine here self.pars['scale'] = self.pars.get('scale', 1) # This avoids handling the first PSF as a special case self.pars['A_1'] = self.pars.get('A_1', 1) def _read_ascii(self, filename): """Parse file with parameters.""" fh = open(filename) # .readlines() pars = {} for line in fh: try: key, value = line.strip().split()[:2] if key.startswith('#'): continue else: pars[key] = float(value) except ValueError: pass fh.close() return pars def __str__(self): return json.dumps(self.pars, sort_keys=True, indent=4)
[docs] def n_gauss(self): """Count number of Gaussians.""" return len([_ for _ in self.pars.keys() if 'sigma' in _])
[docs] def dpdtheta2(self, theta2): """dp / dtheta2 at position theta2 = theta ^ 2.""" theta2 = np.asarray(theta2, 'f') total = np.zeros_like(theta2) for ii in range(1, self.n_gauss() + 1): A = self.pars['A_{}'.format(ii)] sigma = self.pars['sigma_{}'.format(ii)] total += A * np.exp(-theta2 / (2 * sigma ** 2)) return self.pars['scale'] * total
[docs] def to_sherpa(self, binsz): """Convert parameters to Sherpa format. @param binsz: Bin size (deg) @return: dict of Sherpa parameters Note: Sherpa uses the following function f(x,y) = f(r) = A exp[-f(r/F)^2] f = 2.7725887 = 4log2 relates the full-width at half-maximum F to the Gaussian sigma. Note: The sigma parameters in this class are in deg, but in Sherpa we use pix, so we convert here. For further explanations on how to convert 1D to 2D Gaussian, see the docstring of GCTAResponse::psf_dummy in GammaLib Input is unnormalized anyways, so we don't care about absolute PSF normalization here, Sherpa automatically renormalizes. -> Check!""" pars = {} # scale = self.pars['scale'] for ii in range(1, self.n_gauss() + 1): d = {} A = self.pars['A_{}'.format(ii)] sigma = self.pars['sigma_{}'.format(ii)] d['ampl'] = A d['fwhm'] = gaussian_sigma_to_fwhm * sigma / binsz name = 'psf{}'.format(ii) pars[name] = d return pars
[docs] def to_file(self, filename, binsz, fmt='json'): """Convert parameters to Sherpa format and write them to a JSON file. """ if fmt == 'json': pars = self.to_sherpa(binsz) json.dump(pars, open(filename, 'w'), sort_keys=True, indent=4) elif fmt == 'ascii': fh = open(filename, 'w') for name, value in self.pars.items(): fh.write('{} {}\n'.format(name, value)) fh.close()
[docs] def to_MultiGauss2D(self, normalize=True): """Use this to compute containment angles and fractions. Note: We have to set norm = 2 * A * sigma ^ 2, because in MultiGauss2D norm represents the integral, and in HESS A represents the amplitude at 0.""" sigmas, norms = [], [] for ii in range(1, self.n_gauss() + 1): A = self.pars['A_{}'.format(ii)] sigma = self.pars['sigma_{}'.format(ii)] norm = self.pars['scale'] * 2 * A * sigma ** 2 sigmas.append(sigma) norms.append(norm) m = MultiGauss2D(sigmas, norms) if normalize: m.normalize() return m
[docs] def containment_radius(self, containment_fraction): """Convolve this PSF with a Gaussian source of width sigma, then compute the containment angle of that distribution. """ m = self.to_MultiGauss2D(normalize=True) theta = m.containment_radius(containment_fraction) return theta
[docs]def multi_gauss_psf_kernel(psf_parameters, BINSZ=0.02, NEW_BINSZ=0.02, **kwargs): """Create multi-Gauss PSF kernel. The Gaussian PSF components are specified via the amplitude at the center and the FWHM. See the example for the exact format. Parameters ---------- psf_parameters : dict PSF parameters BINSZ : float (0.02) Pixel size used for the given parameters in deg. NEW_BINSZ : float (0.02) New pixel size in deg. USed to change the resolution of the PSF. Returns ------- psf_kernel : `astropy.convolution.Kernel2D` PSF kernel Examples -------- >>> psf_pars = dict() >>> psf_pars['psf1'] = dict(ampl=1, fwhm=2.5) >>> psf_pars['psf2'] = dict(ampl=0.06, fwhm=11.14) >>> psf_pars['psf3'] = dict(ampl=0.47, fwhm=5.16) >>> psf_kernel = multi_gauss_psf_kernel(psf_pars, x_size=51) """ psf = None for ii in range(1, 4): # Convert sigma and amplitude pars = psf_parameters['psf{}'.format(ii)] sigma = gaussian_fwhm_to_sigma * pars['fwhm'] * BINSZ / NEW_BINSZ ampl = 2 * np.pi * sigma ** 2 * pars['ampl'] if psf is None: psf = float(ampl) * Gaussian2DKernel(sigma, **kwargs) else: psf += float(ampl) * Gaussian2DKernel(sigma, **kwargs) psf.normalize() return psf