/*************************************************************************** * GCTACubePsf.cpp - CTA cube analysis point spread function class * * ----------------------------------------------------------------------- * * copyright (C) 2014-2016 by Chia-Chun Lu * * ----------------------------------------------------------------------- * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ***************************************************************************/ /** * @file GCTACubePsf.cpp * @brief CTA cube analysis point spread function class implementation * @author Chia-Chun Lu */ /* __ Includes ___________________________________________________________ */ #ifdef HAVE_CONFIG_H #include #endif #include "GTools.hpp" #include "GMath.hpp" #include "GLog.hpp" #include "GObservations.hpp" #include "GCTACubePsf.hpp" #include "GCTAObservation.hpp" #include "GCTAResponseIrf.hpp" #include "GCTAEventList.hpp" #include "GCTAEventCube.hpp" /* __ Method name definitions ____________________________________________ */ #define G_SET "GCTACubePsf::set(GCTAObservation&)" #define G_FILL "GCTACubePsf::fill(GObservations&, GLog*)" /* __ Macros _____________________________________________________________ */ /* __ Coding definitions _________________________________________________ */ #define G_SMOOTH_PSF //!< Guarantee no singularities #define G_QUADRATIC_BINNING //!< Use quadratic delta spacing /* __ Debug definitions __________________________________________________ */ /* __ Constants __________________________________________________________ */ /*========================================================================== = = = Constructors/destructors = = = ==========================================================================*/ /***********************************************************************//** * @brief Void constructor ***************************************************************************/ GCTACubePsf::GCTACubePsf(void) { // Initialise class members init_members(); // Return return; } /***********************************************************************//** * @brief Copy constructor * * @param[in] cube Point spread function. ***************************************************************************/ GCTACubePsf::GCTACubePsf(const GCTACubePsf& cube) { // Initialise class members init_members(); // Copy members copy_members(cube); // Return return; } /***********************************************************************//** * @brief File constructor * * @param[in] filename PSF cube filename. * * Construct PSF cube by loading the information from a PSF cube file. ***************************************************************************/ GCTACubePsf::GCTACubePsf(const GFilename& filename) { // Initialise class members init_members(); // Load PSF cube from file load(filename); // Return return; } /***********************************************************************//** * @brief Event cube constructor * * @param[in] cube Event cube. * @param[in] dmax Maximum delta (deg). * @param[in] ndbins Number of delta bins. * * Construct PSF cube using the same binning and sky projection that is * used for the event cube. ***************************************************************************/ GCTACubePsf::GCTACubePsf(const GCTAEventCube& cube, const double& dmax, const int& ndbins) { // Initialise class members init_members(); // Store energy boundaries int nebins = cube.ebounds().size(); for (int i = 0; i < nebins; ++i) { m_energies.append(cube.ebounds().emin(i)); } if (nebins > 0) { m_energies.append(cube.ebounds().emax(nebins-1)); } // Set GNodeArray used for interpolation set_eng_axis(); // Set delta node array m_deltas.clear(); for (int i = 0; i < ndbins; ++i) { #if defined(G_QUADRATIC_BINNING) double binsize = std::sqrt(dmax) / double(ndbins); double delta = binsize * (double(i) + 0.5); // avoid central singularity delta *= delta; #else double binsize = dmax / double(ndbins); double delta = binsize * (double(i) + 0.5); // avoid central singularity #endif m_deltas.append(delta); } // Set delta node array for computation set_delta_axis(); // Compute number of sky maps int nmaps = m_energies.size() * m_deltas.size(); // Set PSF cube to event cube m_cube = cube.counts(); // Set appropriate number of skymaps m_cube.nmaps(nmaps); // Set cube shape m_cube.shape(m_deltas.size(), m_energies.size()); // Set all PSF cube pixels to zero as we want to have a clean map // upon construction m_cube = 0.0; // Return return; } /***********************************************************************//** * @brief Mean PSF cube constructor * * @param[in] wcs World Coordinate System. * @param[in] coords Coordinate System (CEL or GAL). * @param[in] x X coordinate of sky map centre (deg). * @param[in] y Y coordinate of sky map centre (deg). * @param[in] dx Pixel size in x direction at centre (deg/pixel). * @param[in] dy Pixel size in y direction at centre (deg/pixel). * @param[in] nx Number of pixels in x direction. * @param[in] ny Number of pixels in y direction. * @param[in] energies Energies. * @param[in] dmax Maximum delta (deg). * @param[in] ndbins Number of delta bins. * * Constructs a mean PSF cube by computing the mean PSF from all CTA * observations found in the observation container. ***************************************************************************/ GCTACubePsf::GCTACubePsf(const std::string& wcs, const std::string& coords, const double& x, const double& y, const double& dx, const double& dy, const int& nx, const int& ny, const GEnergies& energies, const double& dmax, const int& ndbins) { // Initialise class members init_members(); // Store energies m_energies = energies; // Set energy node array set_eng_axis(); // Set delta node array m_deltas.clear(); for (int i = 0; i < ndbins; ++i) { #if defined(G_QUADRATIC_BINNING) double binsize = std::sqrt(dmax) / double(ndbins); double delta = binsize * (double(i) + 0.5); // avoid central singularity delta *= delta; #else double binsize = dmax / double(ndbins); double delta = binsize * (double(i) + 0.5); // avoid central singularity #endif m_deltas.append(delta); } // Set delta node array for computation set_delta_axis(); // Compute number of sky maps int nmaps = m_energies.size() * m_deltas.size(); // Create sky map m_cube = GSkyMap(wcs, coords, x, y, dx, dy, nx, ny, nmaps); // Set cube shape m_cube.shape(m_deltas.size(), m_energies.size()); // Return return; } /***********************************************************************//** * @brief Destructor ***************************************************************************/ GCTACubePsf::~GCTACubePsf(void) { // Free members free_members(); // Return return; } /*========================================================================== = = = Operators = = = ==========================================================================*/ /***********************************************************************//** * @brief Assignment operator * * @param[in] cube Mean PSF cube. * @return Mean PSF cube. ***************************************************************************/ GCTACubePsf& GCTACubePsf::operator= (const GCTACubePsf& cube) { // Execute only if object is not identical if (this != &cube) { // Free members free_members(); // Initialise private members init_members(); // Copy members copy_members(cube); } // endif: object was not identical // Return this object return *this; } /***********************************************************************//** * @brief Return point spread function (in units of sr^-1) * * @param[in] dir Coordinate of the true photon position. * @param[in] delta Angular separation between true and measured photon * directions (rad). * @param[in] energy Energy of the true photon. * @return point spread function (in units of sr^-1) * * Returns the point spread function for a given angular separation in units * of sr^-1 for a given energy and coordinate. ***************************************************************************/ double GCTACubePsf::operator()(const GSkyDir& dir, const double& delta, const GEnergy& energy) const { // Update indices and weighting factors for interpolation update(delta, energy.log10TeV()); // Perform bi-linear interpolation double psf = m_wgt1 * m_cube(dir, m_inx1) + m_wgt2 * m_cube(dir, m_inx2) + m_wgt3 * m_cube(dir, m_inx3) + m_wgt4 * m_cube(dir, m_inx4); // Make sure that PSF does not become negative if (psf < 0.0) { psf = 0.0; } // Return PSF return psf; } /*========================================================================== = = = Public methods = = = ==========================================================================*/ /***********************************************************************//** * @brief Clear instance * * This method properly resets the object to an initial state. ***************************************************************************/ void GCTACubePsf::clear(void) { // Free class members free_members(); // Initialise members init_members(); // Return return; } /***********************************************************************//** * @brief Clone instance * * @return Deep copy of mean PSF instance. ***************************************************************************/ GCTACubePsf* GCTACubePsf::clone(void) const { return new GCTACubePsf(*this); } /***********************************************************************//** * @brief Set PSF cube from one CTA observation * * @param[in] obs CTA observation. ***************************************************************************/ void GCTACubePsf::set(const GCTAObservation& obs) { // Clear PSF cube clear_cube(); // Only continue if we have an unbinned observation if (obs.eventtype() == "EventList") { // Get references on CTA response and pointing direction const GCTAResponseIrf* rsp = dynamic_cast(obs.response()); const GSkyDir& pnt = obs.pointing().dir(); // Continue only if response is valid if (rsp != NULL) { // Loop over all pixels in sky map for (int pixel = 0; pixel < m_cube.npix(); ++pixel) { // Get pixel sky direction GSkyDir dir = m_cube.inx2dir(pixel); // Compute theta angle with respect to pointing direction // in radians double theta = pnt.dist(dir); // Loop over all exposure cube energy bins for (int iebin = 0; iebin < m_energies.size(); ++iebin){ // Get logE/TeV double logE = m_energies[iebin].log10TeV(); // Loop over delta values for (int idelta = 0; idelta < m_deltas.size(); ++idelta) { // Compute delta in radians double delta = m_deltas[idelta] * gammalib::deg2rad; // Set map index int imap = offset(idelta, iebin); // Set PSF cube m_cube(pixel, imap) = rsp->psf(delta, theta, 0.0, 0.0, 0.0, logE); } // endfor: looped over delta bins } // endfor: looped over energy bins } // endfor: looped over all pixels } // endif: response was valid } // endif: observation was unbinned // Compile option: guarantee smooth Psf #if defined(G_SMOOTH_PSF) set_to_smooth(); #endif // Return return; } /***********************************************************************//** * @brief Fill PSF cube from observation container * * @param[in] obs Observation container. * @param[in] log Pointer towards logger (default: NULL). ***************************************************************************/ void GCTACubePsf::fill(const GObservations& obs, GLog* log) { // Clear PSF cube clear_cube(); // Initialise skymap for exposure weight accumulation GSkyMap exposure(m_cube); // Loop over all observations in container for (int i = 0; i < obs.size(); ++i) { // Get observation and continue only if it is a CTA observation const GCTAObservation* cta = dynamic_cast(obs[i]); // Skip observation if it's not CTA if (cta == NULL) { if (log != NULL) { *log << "Skipping "; *log << obs[i]->instrument(); *log << " observation "; *log << "\"" << obs[i]->name() << "\""; *log << " (id=" << obs[i]->id() << ")" << std::endl; } continue; } // Skip observation if we have a binned observation if (cta->eventtype() == "CountsCube") { if (log != NULL) { *log << "Skipping binned "; *log << cta->instrument(); *log << " observation "; *log << "\"" << cta->name() << "\""; *log << " (id=" << cta->id() << ")" << std::endl; } continue; } // Get references on CTA response and pointing direction const GCTAResponseIrf* rsp = dynamic_cast(cta->response()); const GSkyDir& pnt = cta->pointing().dir(); // Skip observation if we don't have an unbinned observation if (rsp == NULL) { if (log != NULL) { *log << "WARNING: "; *log << cta->instrument(); *log << " observation \"" << cta->name(); *log << "\" (id=" << cta->id() << ")"; *log << " contains no IRF response."; *log << " Skipping this observation." << std::endl; } continue; } // Announce observation usage if (log != NULL) { *log << "Including "; *log << cta->instrument(); *log << " observation \"" << cta->name(); *log << "\" (id=" << cta->id() << ")"; *log << " in point spread function cube computation." << std::endl; } // Loop over all pixels in sky map for (int pixel = 0; pixel < m_cube.npix(); ++pixel) { // Get pixel sky direction GSkyDir dir = m_cube.inx2dir(pixel); // Compute theta angle with respect to pointing direction in radians double theta = pnt.dist(dir); // Loop over all energy bins for (int iebin = 0; iebin < m_energies.size(); ++iebin) { // Get logE/TeV double logE = m_energies[iebin].log10TeV(); // Compute exposure weight double weight = rsp->aeff(theta, 0.0, 0.0, 0.0, logE) * cta->livetime(); // Accumulate weights exposure(pixel, iebin) += weight; // Loop over delta values for (int idelta = 0; idelta < m_deltas.size(); ++idelta) { // Compute delta in radians double delta = m_deltas[idelta] * gammalib::deg2rad; // Set map index int imap = offset(idelta, iebin); // Add on PSF cube m_cube(pixel, imap) += rsp->psf(delta, theta, 0.0, 0.0, 0.0, logE) * weight; } // endfor: looped over delta bins } // endfor: looped over energy bins } // endfor: looped over all pixels } // endfor: looped over observations // Compute mean PSF cube by dividing though the weights for (int pixel = 0; pixel < m_cube.npix(); ++pixel) { for (int iebin = 0; iebin < m_energies.size(); ++iebin) { if (exposure(pixel, iebin) > 0.0) { double norm = 1.0 / exposure(pixel, iebin); for (int idelta = 0; idelta < m_deltas.size(); ++idelta) { int imap = offset(idelta, iebin); m_cube(pixel, imap) *= norm; } } else { for (int idelta = 0; idelta < m_deltas.size(); ++idelta) { int imap = offset(idelta, iebin); m_cube(pixel, imap) = 0.0; } } } } // Compile option: guarantee smooth Psf #if defined(G_SMOOTH_PSF) set_to_smooth(); #endif // Return return; } /***********************************************************************//** * @brief Read PSF cube from FITS object * * @param[in] fits FITS object. * * Read the PSF cube from a FITS object. ***************************************************************************/ void GCTACubePsf::read(const GFits& fits) { // Clear object clear(); // Get HDUs const GFitsImage& hdu_psfcube = *fits.image("Primary"); const GFitsTable& hdu_energies = *fits.table("ENERGIES"); const GFitsTable& hdu_deltas = *fits.table("DELTAS"); // Read cube m_cube.read(hdu_psfcube); // Read energies m_energies.read(hdu_energies); // Read delta nodes m_deltas.read(hdu_deltas); // Set delta node array for computation set_delta_axis(); // Set energy node array set_eng_axis(); // Compile option: guarantee smooth Psf #if defined(G_SMOOTH_PSF) set_to_smooth(); #endif // Return return; } /***********************************************************************//** * @brief Write CTA PSF cube into FITS object. * * @param[in] fits FITS object. * * Write the CTA PSF cube into a FITS object. ***************************************************************************/ void GCTACubePsf::write(GFits& fits) const { // Write cube m_cube.write(fits); // Write energies m_energies.write(fits); // Write delta nodes m_deltas.write(fits, "DELTAS"); // Set the nodes unit to "deg" (*fits.table("DELTAS"))["Value"]->unit("deg"); // Return return; } /***********************************************************************//** * @brief Load PSF cube from FITS file * * @param[in] filename Performance table file name. * * Loads the PSF cube from a FITS file into the object. ***************************************************************************/ void GCTACubePsf::load(const GFilename& filename) { // Open FITS file GFits fits(filename); // Read PSF cube read(fits); // Close FITS file fits.close(); // Store filename m_filename = filename; // Return return; } /***********************************************************************//** * @brief Save PSF cube into FITS file * * @param[in] filename PSF cube FITS file name. * @param[in] clobber Overwrite existing file? (default: false) * * Save the PSF cube into a FITS file. ***************************************************************************/ void GCTACubePsf::save(const GFilename& filename, const bool& clobber) const { // Create FITS file GFits fits; // Write PSF cube write(fits); // Save FITS file fits.saveto(filename, clobber); // Store filename m_filename = filename; // Return return; } /***********************************************************************//** * @brief Print PSF cube information * * @param[in] chatter Chattiness (defaults to NORMAL). * @return String containing PSF cube information. ***************************************************************************/ std::string GCTACubePsf::print(const GChatter& chatter) const { // Initialise result string std::string result; // Continue only if chatter is not silent if (chatter != SILENT) { // Append header result.append("=== GCTACubePsf ==="); // Append information result.append("\n"+gammalib::parformat("Filename")+m_filename); // Append energies if (m_energies.size() > 0) { result.append("\n"+m_energies.print(chatter)); } else { result.append("\n"+gammalib::parformat("Energies") + "not defined"); } // Append number of delta bins result.append("\n"+gammalib::parformat("Number of delta bins") + gammalib::str(m_deltas.size())); // Append delta range result.append("\n"+gammalib::parformat("Delta range")); if (m_deltas.size() > 0) { result.append(gammalib::str(m_deltas[0])); result.append(" - "); result.append(gammalib::str(m_deltas[m_deltas.size()-1])); result.append(" deg"); } else { result.append("not defined"); } // Append skymap definition result.append("\n"+m_cube.print(chatter)); } // endif: chatter was not silent // Return result return result; } /*========================================================================== = = = Private methods = = = ==========================================================================*/ /***********************************************************************//** * @brief Initialise class members ***************************************************************************/ void GCTACubePsf::init_members(void) { // Initialise members m_filename.clear(); m_cube.clear(); m_energies.clear(); m_elogmeans.clear(); m_deltas.clear(); m_deltas_cache.clear(); m_quadratic_binning = false; // Initialise cache m_inx1 = 0; m_inx2 = 0; m_inx3 = 0; m_inx4 = 0; m_wgt1 = 0.0; m_wgt2 = 0.0; m_wgt3 = 0.0; m_wgt4 = 0.0; // Return return; } /***********************************************************************//** * @brief Copy class members * * @param[in] cube PSF cube. ***************************************************************************/ void GCTACubePsf::copy_members(const GCTACubePsf& cube) { // Copy members m_filename = cube.m_filename; m_cube = cube.m_cube; m_energies = cube.m_energies; m_elogmeans = cube.m_elogmeans; m_deltas = cube.m_deltas; m_deltas_cache = cube.m_deltas_cache; m_quadratic_binning = cube.m_quadratic_binning; // Copy cache m_inx1 = cube.m_inx1; m_inx2 = cube.m_inx2; m_inx3 = cube.m_inx3; m_inx4 = cube.m_inx4; m_wgt1 = cube.m_wgt1; m_wgt2 = cube.m_wgt2; m_wgt3 = cube.m_wgt3; m_wgt4 = cube.m_wgt4; // Return return; } /***********************************************************************//** * @brief Delete class members ***************************************************************************/ void GCTACubePsf::free_members(void) { // Return return; } /***********************************************************************//** * @brief Clear all pixels in the PSF cube ***************************************************************************/ void GCTACubePsf::clear_cube(void) { // Loop over all maps for (int imap = 0; imap < m_cube.nmaps(); ++imap) { // Loop over all pixels in sky map for (int pixel = 0; pixel < m_cube.npix(); ++pixel) { // Reset cube value to zero m_cube(pixel, imap) = 0.0; } // endfor: looped over all pixels } // endfor: looped over maps // Return return; } /***********************************************************************//** * @brief Update PSF parameter cache * * @param[in] delta Angular separation between true and measured photon * directions (radians). * @param[in] logE Log10 true photon energy (TeV). * * This method updates the PSF parameter cache. ***************************************************************************/ void GCTACubePsf::update(const double& delta, const double& logE) const { // Set node array for delta interpolation if (m_quadratic_binning) { m_deltas_cache.set_value(std::sqrt(delta)); } else { m_deltas_cache.set_value(delta); } // Set node array for energy interpolation m_elogmeans.set_value(logE); // Set indices for bi-linear interpolation m_inx1 = offset(m_deltas_cache.inx_left(), m_elogmeans.inx_left()); m_inx2 = offset(m_deltas_cache.inx_left(), m_elogmeans.inx_right()); m_inx3 = offset(m_deltas_cache.inx_right(), m_elogmeans.inx_left()); m_inx4 = offset(m_deltas_cache.inx_right(), m_elogmeans.inx_right()); // Set weighting factors for bi-linear interpolation m_wgt1 = m_deltas_cache.wgt_left() * m_elogmeans.wgt_left(); m_wgt2 = m_deltas_cache.wgt_left() * m_elogmeans.wgt_right(); m_wgt3 = m_deltas_cache.wgt_right() * m_elogmeans.wgt_left(); m_wgt4 = m_deltas_cache.wgt_right() * m_elogmeans.wgt_right(); // Return return; } /***********************************************************************//** * @brief Set nodes for delta axis in radians * * Set the delta axis nodes in radians. If a quadratic binning is detected, * the axis is set to sqrt(delta) so that a linear interpolation scheme * can be used which is much faster than a quadratic scheme. * * @todo Check that none of the axis boundaries is non-positive. ***************************************************************************/ void GCTACubePsf::set_delta_axis(void) { // Initialise computation members m_deltas_cache.clear(); m_quadratic_binning = false; // Set up a linear array that represents the square root of the delta // values. If this array corresponds within some numerical precision // to the actual delta values, will will use these values for the // interpolation (the m_quadratic_binning member will be set to true). // Otherwise, the original delta values will be kept and the // m_quadratic_binning member will be set to false. int ndbins = m_deltas.size(); if (ndbins > 1) { m_quadratic_binning = true; double binsize = (std::sqrt(m_deltas[ndbins-1] * gammalib::deg2rad) - std::sqrt(m_deltas[0] * gammalib::deg2rad)) / double(ndbins-1); for (int i = 0; i < ndbins; ++i) { double delta = binsize * (double(i) + 0.5); double diff = std::abs(delta*delta-m_deltas[i] * gammalib::deg2rad); if (diff > 1.0e-6) { m_quadratic_binning = false; break; } m_deltas_cache.append(delta); } } // If we do not have square root binning then use the original values // but convert them to radians if (!m_quadratic_binning) { m_deltas_cache.clear(); for (int i = 0; i < m_deltas.size(); ++i) { m_deltas_cache.append(m_deltas[i] * gammalib::deg2rad); } } // Return return; } /***********************************************************************//** * @brief Set nodes for a logarithmic (base 10) energy axis * * * Set axis nodes so that each node is the logarithmic mean of the lower and * upper energy boundary, i.e. * \f[ n_i = \log \sqrt{{\rm LO}_i \times {\rm HI}_i} \f] * where * \f$n_i\f$ is node \f$i\f$, * \f${\rm LO}_i\f$ is the lower bin boundary for bin \f$i\f$, and * \f${\rm HI}_i\f$ is the upper bin boundary for bin \f$i\f$. * * @todo Check that none of the axis boundaries is non-positive. ***************************************************************************/ void GCTACubePsf::set_eng_axis(void) { // Get number of bins int bins = m_energies.size(); // Clear node array m_elogmeans.clear(); // Compute nodes for (int i = 0; i < bins; ++i) { // Get logE/TeV m_elogmeans.append(m_energies[i].log10TeV()); } // endfor: looped over energy bins // Return return; } /***********************************************************************//** * @brief Pad the last delta bins with zero * * Zero padding of the last delta bins assures that the Psf goes to zero * without any step at the last delta value. ***************************************************************************/ void GCTACubePsf::set_to_smooth(void) { // Continue only if there are delta bins if (m_deltas.size() > 2) { // Set first delta bin to value of second bin (capped Psf) for (int iebin = 0; iebin < m_energies.size(); ++iebin) { int isrc = offset(1, iebin); int idst = offset(0, iebin); for (int pixel = 0; pixel < m_cube.npix(); ++pixel) { m_cube(pixel, idst) = m_cube(pixel, isrc); } } // Get index of last delta bin int idelta = m_deltas.size()-1; // Pad mean PSF with zeros in the last delta bin for (int iebin = 0; iebin < m_energies.size(); ++iebin) { int imap = offset(idelta, iebin); for (int pixel = 0; pixel < m_cube.npix(); ++pixel) { m_cube(pixel, imap) = 0.0; } } } // endif: there were bins to pad // Return return; }