/*************************************************************************** * GCTAResponse.cpp - CTA Response class * * ----------------------------------------------------------------------- * * copyright (C) 2010-2013 by Juergen Knoedlseder * * ----------------------------------------------------------------------- * * * * 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 GCTAResponse.cpp * @brief CTA response class implementation * @author Juergen Knoedlseder */ /* __ Includes ___________________________________________________________ */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include "GFits.hpp" #include "GTools.hpp" #include "GMath.hpp" #include "GIntegral.hpp" #include "GCaldb.hpp" #include "GModelSpatialRadial.hpp" #include "GModelSpatialElliptical.hpp" #include "GCTAObservation.hpp" #include "GCTAResponse.hpp" #include "GCTAResponse_helpers.hpp" #include "GCTAPointing.hpp" #include "GCTAEventAtom.hpp" #include "GCTAEventList.hpp" #include "GCTARoi.hpp" #include "GCTAException.hpp" #include "GCTASupport.hpp" #include "GCTAAeff2D.hpp" #include "GCTAAeffArf.hpp" #include "GCTAAeffPerfTable.hpp" #include "GCTAPsf2D.hpp" #include "GCTAPsfVector.hpp" #include "GCTAPsfPerfTable.hpp" #include "GCTAPsfKing.hpp" #include "GCTAAeff.hpp" #include "GCTAPsf.hpp" #include "GCTAEdisp.hpp" /* __ Method name definitions ____________________________________________ */ #define G_CALDB "GCTAResponse::caldb(std::string&)" #define G_IRF "GCTAResponse::irf(GInstDir&, GEnergy&, GTime&, GSkyDir&,"\ " GEnergy&, GTime&, GObservation&)" #define G_NPRED "GCTAResponse::npred(GSkyDir&, GEnergy&, GTime&,"\ " GObservation&)" #define G_MC "GCTAResponse::mc(double&,GPhoton&,GPointing&,GRan&)" #define G_IRF_RADIAL "GCTAResponse::irf_radial(GEvent&, GSource&,"\ " GObservation&)" #define G_IRF_ELLIPTICAL "GCTAResponse::irf_elliptical(GEvent&, GSource&,"\ " GObservation&)" #define G_IRF_DIFFUSE "GCTAResponse::irf_diffuse(GEvent&, GSource&,"\ " GObservation&)" #define G_NPRED_RADIAL "GCTAResponse::npred_radial(GSource&, GObservation&)" #define G_NPRED_ELLIPTICAL "GCTAResponse::npred_elliptical(GSource&,"\ " GObservation&)" #define G_NPRED_DIFFUSE "GCTAResponse::npred_diffuse(GSource&,"\ " GObservation&)" #define G_READ "GCTAResponse::read_performance_table(std::string&)" /* __ Macros _____________________________________________________________ */ /* __ Coding definitions _________________________________________________ */ #define G_USE_IRF_CACHE //!< Use IRF cache in irf_diffuse method #define G_USE_NPRED_CACHE //!< Use Npred cache in npred_diffuse method /* __ Debug definitions __________________________________________________ */ //#define G_DEBUG_IRF_RADIAL //!< Debug irf_radial method //#define G_DEBUG_IRF_DIFFUSE //!< Debug irf_diffuse method //#define G_DEBUG_IRF_ELLIPTICAL //!< Debug irf_elliptical method //#define G_DEBUG_NPRED_RADIAL //!< Debug npred_radial method //#define G_DEBUG_NPRED_DIFFUSE //!< Debug npred_diffuse method //#define G_DEBUG_NPRED_ELLIPTICAL //!< Debug npred_elliptical method //#define G_DEBUG_PRINT_AEFF //!< Debug print() Aeff method //#define G_DEBUG_PRINT_PSF //!< Debug print() Psf method //#define G_DEBUG_PSF_DUMMY_SIGMA //!< Debug psf_dummy_sigma method /* __ Constants __________________________________________________________ */ /*========================================================================== = = = Constructors/destructors = = = ==========================================================================*/ /***********************************************************************//** * @brief Void constructor * * Constructs void CTA response. ***************************************************************************/ GCTAResponse::GCTAResponse(void) : GResponse() { // Initialise members init_members(); // Return return; } /***********************************************************************//** * @brief Copy constructor * * @param[in] rsp CTA response. * * Constructs CTA response by making a deep copy of an existing object. **************************************************************************/ GCTAResponse::GCTAResponse(const GCTAResponse& rsp) : GResponse(rsp) { // Initialise members init_members(); // Copy members copy_members(rsp); // Return return; } /***********************************************************************//** * @brief Response constructor * * @param[in] rspname Response file name. * @param[in] caldb Calibration database path (defaults to ""). * * Create instance of a CTA response object by specifying the response file * name and the calibration database path. * * If an empty string is passed as calibration database path, the method will * use the CALDB environment variable to determine calibration database path. * This is done in the method GCTAResponse::caldb which makes use of a * GCaldb object to locate the calibration database. ***************************************************************************/ GCTAResponse::GCTAResponse(const std::string& rspname, const std::string& caldb) : GResponse() { // Initialise members init_members(); // Set calibration database this->caldb(caldb); // Load IRF this->load(rspname); // Return return; } /***********************************************************************//** * @brief Destructor * * Destroys instance of CTA response object. ***************************************************************************/ GCTAResponse::~GCTAResponse(void) { // Free members free_members(); // Return return; } /*========================================================================== = = = Operators = = = ==========================================================================*/ /***********************************************************************//** * @brief Assignment operator * * @param[in] rsp CTA response. * @return CTA response. * * Assigns CTA response object to another CTA response object. The assignment * performs a deep copy of all information, hence the original object from * which the assignment has been performed can be destroyed after this * operation without any loss of information. ***************************************************************************/ GCTAResponse& GCTAResponse::operator=(const GCTAResponse& rsp) { // Execute only if object is not identical if (this != &rsp) { // Copy base class members this->GResponse::operator=(rsp); // Free members free_members(); // Initialise members init_members(); // Copy members copy_members(rsp); } // endif: object was not identical // Return this object return *this; } /*========================================================================== = = = Public methods = = = ==========================================================================*/ /***********************************************************************//** * @brief Clear instance * * Clears CTA response object by resetting all members to an initial state. * Any information that was present in the object before will be lost. ***************************************************************************/ void GCTAResponse::clear(void) { // Free class members (base and derived classes, derived class first) free_members(); this->GResponse::free_members(); // Initialise members this->GResponse::init_members(); init_members(); // Return return; } /***********************************************************************//** * @brief Clone instance * * @return Pointer to deep copy of CTA response. * * Creates a clone (deep copy) of a CTA response object. ***************************************************************************/ GCTAResponse* GCTAResponse::clone(void) const { return new GCTAResponse(*this); } /***********************************************************************//** * @brief Return value of instrument response function * * @param[in] event Observed event. * @param[in] photon Incident photon. * @param[in] obs Observation. * * @todo Set polar angle phi of photon in camera system ***************************************************************************/ double GCTAResponse::irf(const GEvent& event, const GPhoton& photon, const GObservation& obs) const { // Retrieve CTA pointing and instrument direction const GCTAPointing& pnt = retrieve_pnt(G_IRF, obs); const GCTAInstDir& dir = retrieve_dir(G_IRF, event); // Get event attributes const GSkyDir& obsDir = dir.dir(); const GEnergy& obsEng = event.energy(); // Get photon attributes const GSkyDir& srcDir = photon.dir(); const GEnergy& srcEng = photon.energy(); const GTime& srcTime = photon.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Get radial offset and polar angles of true photon in camera [radians] double theta = pnt.dir().dist(srcDir); double phi = 0.0; //TODO: Implement Phi dependence // Get log10(E/TeV) of true photon energy. double srcLogEng = srcEng.log10TeV(); // Determine angular separation between true and measured photon // direction in radians double delta = obsDir.dist(srcDir); // Get maximum angular separation for which PSF is significant double delta_max = psf_delta_max(theta, phi, zenith, azimuth, srcLogEng); // Initialise IRF value double irf = 0.0; // Compute only if we're sufficiently close to PSF if (delta <= delta_max) { // Get effective area component irf = aeff(theta, phi, zenith, azimuth, srcLogEng); // Multiply-in PSF if (irf > 0.0) { // Get PSF component irf *= psf(delta, theta, phi, zenith, azimuth, srcLogEng); // Multiply-in energy dispersion if (has_edisp() && irf > 0) { // Get log10(E/TeV) of measured photon energy. double obsLogEng = obsEng.log10TeV(); // Multiply-in energy dispersion irf *= edisp(obsLogEng, theta, phi, zenith, azimuth, srcLogEng); } // endif: energy dispersion was available and PSF was non-zero // Apply deadtime correction irf *= obs.deadc(srcTime); } // endif: Aeff was non-zero } // endif: we were sufficiently close to PSF // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(irf) || gammalib::is_infinite(irf)) { std::cout << "*** ERROR: GCTAResponse::irf:"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", theta=" << theta; std::cout << ", phi=" << phi << ")"; std::cout << std::endl; } #endif // Return IRF value return irf; } /***********************************************************************//** * @brief Return spatial integral of point spread function * * @param[in] photon Incident photon. * @param[in] obs Observation. * * @todo Set polar angle phi of photon in camera system * @todo Write method documentation ***************************************************************************/ double GCTAResponse::npred(const GPhoton& photon, const GObservation& obs) const { // Retrieve CTA observation, ROI and pointing const GCTAObservation& cta = retrieve_obs(G_NPRED, obs); const GCTARoi& roi = retrieve_roi(G_NPRED, obs); const GCTAPointing& pnt = cta.pointing(); // Get photon attributes const GSkyDir& srcDir = photon.dir(); const GEnergy& srcEng = photon.energy(); const GTime& srcTime = photon.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Get radial offset and polar angles of true photon in camera [radians] double theta = pnt.dir().dist(srcDir); double phi = 0.0; //TODO: Implement Phi dependence // Get log10(E/TeV) of true photon energy. double srcLogEng = srcEng.log10TeV(); // Get effectve area components double npred = aeff(theta, phi, zenith, azimuth, srcLogEng); // Multiply-in PSF if (npred > 0.0) { // Get PSF npred *= npsf(srcDir, srcLogEng, srcTime, pnt, roi); // Multiply-in energy dispersion if (has_edisp() && npred > 0.0) { // Get energy dispersion npred *= nedisp(srcDir, srcEng, srcTime, pnt, cta.events()->ebounds()); } // endif: had energy dispersion // Apply deadtime correction npred *= obs.deadc(srcTime); } // endif: had non-zero effective area // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(npred) || gammalib::is_infinite(npred)) { std::cout << "*** ERROR: GCTAResponse::npred:"; std::cout << " NaN/Inf encountered"; std::cout << " (npred=" << npred; std::cout << ", theta=" << theta; std::cout << ", phi=" << phi << ")"; std::cout << std::endl; } #endif // Return Npred return npred; } /***********************************************************************//** * @brief Simulate event from photon * * @param[in] area Simulation surface area. * @param[in] photon Photon. * @param[in] obs Observation. * @param[in] ran Random number generator. * * Simulates a CTA event using the response function from an incident photon. * If the event is not detected a NULL pointer is returned. * * The method also applies a deadtime correction using a Monte Carlo process, * taking into account temporal deadtime variations. For this purpose, the * method makes use of the time dependent GObservation::deadc method. * * @todo Set polar angle phi of photon in camera system * @todo Implement energy dispersion ***************************************************************************/ GCTAEventAtom* GCTAResponse::mc(const double& area, const GPhoton& photon, const GObservation& obs, GRan& ran) const { // Initialise event GCTAEventAtom* event = NULL; // Retrieve CTA pointing const GCTAPointing& pnt = retrieve_pnt(G_MC, obs); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Get radial offset and polar angles of true photon in camera [radians] double theta = pnt.dir().dist(photon.dir()); double phi = 0.0; //TODO Implement Phi dependence // Compute effective area for photon double srcLogEng = photon.energy().log10TeV(); double effective_area = aeff(theta, phi, zenith, azimuth, srcLogEng); // Compute limiting value double ulimite = effective_area / area; // Continue only if event is detected if (ran.uniform() <= ulimite) { // Apply deadtime correction double deadc = obs.deadc(photon.time()); if (deadc >= 1.0 || ran.uniform() <= deadc) { // Simulate offset from photon arrival direction double delta = psf()->mc(ran, srcLogEng, theta, phi, zenith, azimuth) * gammalib::rad2deg; double alpha = 360.0 * ran.uniform(); // Rotate sky direction by offset GSkyDir sky_dir = photon.dir(); sky_dir.rotate_deg(alpha, delta); // Set measured photon arrival direction GCTAInstDir inst_dir; inst_dir.dir(sky_dir); // Allocate event event = new GCTAEventAtom; // Set event attributes event->dir(inst_dir); event->energy(photon.energy()); event->time(photon.time()); } // endif: detector was alive } // endif: event was detected // Return event return event; } /***********************************************************************//** * @brief Set path to the calibration database * * @param[in] caldb Path to calibration database * * This method stores the CALDB root directory as the path to the CTA * calibration database. Once the final calibration format has been decided * on, this method should be adjusted. ***************************************************************************/ void GCTAResponse::caldb(const std::string& caldb) { // Allocate calibration database GCaldb db(caldb); // Store the path to the calibration database m_caldb = db.dir(); // Return return; } /***********************************************************************//** * @brief Load CTA response. * * @param[in] irfname Name of CTA response (without any file extension). ***************************************************************************/ void GCTAResponse::load(const std::string& irfname) { // Save calibration database name std::string caldb = m_caldb; // Clear instance clear(); // Restore calibration database name m_caldb = caldb; // Build filename. If file is not a FITS file then add ".dat" extension // which is typical for a performance table. std::string filename = m_caldb + "/" + irfname; if (irfname.rfind(".fits") == std::string::npos && irfname.rfind(".fits.gz") == std::string::npos) { filename += ".dat"; } // Load effective area load_aeff(filename); // Load point spread function load_psf(filename); // Remove theta cut GCTAAeffArf* arf = const_cast(dynamic_cast(m_aeff)); if (arf != NULL) { arf->remove_thetacut(*this); } // Store response name m_rspname = irfname; // Return return; } /***********************************************************************//** * @brief Load effective area * * @param[in] filename Effective area filename. * * This method allocates an effective area instance and load the effective * area information from a response file. The following response file formats * are supported: * * (1) A CTA performance table. This is an ASCII file which specifies the * on-axis effective area as function of energy. * * (2) A ARF FITS file. This is a FITS file which stores the effective area * in a vector. * * (3) A CTA response table. This is a FITS file which specifies the * effective area as function of energy and offset angle. * * This method examines the file, and depending on the detected format, * allocates the appropriate effective area class and loads the data. * * First, the method checks whether the file is a FITS file or not. If the * file is not a FITS file, it is assumed that the file is an ASCII * performance table. If the file is a FITS file, the number of rows found * in the table is used to distinguish between an ARF (multiple rows) and * a CTA response table (single row). * * @todo Implement a method that checks if a file is a FITS file instead * of using try-catch. ***************************************************************************/ void GCTAResponse::load_aeff(const std::string& filename) { // Free any existing effective area instance if (m_aeff != NULL) delete m_aeff; m_aeff = NULL; // Try opening the file as a FITS file try { // Open FITS file GFits file(filename); // If file contains an "EFFECTIVE AREA" extension then load it // as CTA response table if (file.contains("EFFECTIVE AREA")) { file.close(); m_aeff = new GCTAAeff2D(filename); } // ... else if file contains a "SPECRESP" extension then load it // as ARF else if (file.contains("SPECRESP")) { file.close(); m_aeff = new GCTAAeffArf(filename); } } // If FITS file opening failed then assume that we have a performance // table catch (GException::fits_open_error &e) { m_aeff = new GCTAAeffPerfTable(filename); } // Return return; } /***********************************************************************//** * @brief Load CTA PSF vector * * @param[in] filename FITS file name. * * This method loads CTA PSF information from a FITS table. Two FITS file * formats are supported by the method: * * (1) A PSF vector, stored in a format similar to an ARF vector. It is * expected that this format is only a preliminary format that will * disappear in the future (m_psf_version=-9). * * (2) A PSF response table, where PSF parameters are given as function of * energy, offset angle, and eventually some other parameters. This format * is expected to be the definitive response format for CTA * (m_psf_version=-8). * * This method examines the FITS file, and depending on the detected format, * calls the relevant methods. Detection is done by the number of rows that * are found in the table. A single row means that we deal with a response * table, while multiple rows mean that we deal with a response vector. * * @todo Implement a method that checks if a file is a FITS file instead * of using try-catch. ***************************************************************************/ void GCTAResponse::load_psf(const std::string& filename) { // Free any existing point spread function instance if (m_psf != NULL) delete m_psf; m_psf = NULL; // Try opening the file as a FITS file try { // Open FITS file GFits file(filename); // If file contains a "POINT SPREAD FUNCTION" extension then load it // as either a King profile PSF or a 2D PSF if (file.contains("POINT SPREAD FUNCTION")) { const GFitsTable& table = *file.table("POINT SPREAD FUNCTION"); if (table.contains("GAMMA") && table.contains("SIGMA")) { file.close(); m_psf = new GCTAPsfKing(filename); } else if (table.contains("SCALE") && table.contains("SIGMA_1") && table.contains("AMPL_2") && table.contains("SIGMA_2") && table.contains("AMPL_3") && table.contains("SIGMA_3")) { file.close(); m_psf = new GCTAPsf2D(filename); } else { file.close(); } } // ... else load it has PSF vector else { file.close(); m_psf = new GCTAPsfVector(filename); } } // If FITS file opening failed then assume that we have a performance // table catch (GException::fits_open_error &e) { m_psf = new GCTAPsfPerfTable(filename); } // Return return; } /***********************************************************************//** * @brief Set offset angle dependence (degrees) * * @param[in] sigma Offset angle dependence value (degrees). * * Set the offset angle dependence for 1D effective area functions. The * method set the sigma value in case that the effective area function * is of type GCTAAeffArf or GCTAAeffPerfTable. Otherwise, nothing will * be done. ***************************************************************************/ void GCTAResponse::offset_sigma(const double& sigma) { // If effective area is an ARF then set offset angle GCTAAeffArf* arf = dynamic_cast(m_aeff); if (arf != NULL) { arf->sigma(sigma); } // If effective area is a performance table then set offset angle GCTAAeffPerfTable* prf = dynamic_cast(m_aeff); if (prf != NULL) { prf->sigma(sigma); } // Return return; } /***********************************************************************//** * @brief Return offset angle dependence (degrees) * * @return Offset angle dependence value (degrees). * * Return the offset angle dependence for 1D effective area functions. The * method returns the sigma value in case that the effective area function * is of type GCTAAeffArf or GCTAAeffPerfTable. Otherwise, 0.0 will be * returned. ***************************************************************************/ double GCTAResponse::offset_sigma(void) const { // Initialise value double sigma = 0.0; // If effective area is an ARF then get offset angle GCTAAeffArf* arf = dynamic_cast(m_aeff); if (arf != NULL) { sigma = arf->sigma(); } // If effective area is a performance table then get offset angle GCTAAeffPerfTable* prf = dynamic_cast(m_aeff); if (prf != NULL) { sigma = prf->sigma(); } // Return sigma return sigma; } /***********************************************************************//** * @brief Print CTA response information * * @param[in] chatter Chattiness (defaults to NORMAL). * @return String containing CTA response information. ***************************************************************************/ std::string GCTAResponse::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("=== GCTAResponse ==="); // Append response information result.append("\n"+gammalib::parformat("Calibration database")+m_caldb); result.append("\n"+gammalib::parformat("Response name")+m_rspname); result.append("\n"+gammalib::parformat("RMF file name")+m_rmffile); // Append effective area information if (m_aeff != NULL) { result.append("\n"+m_aeff->print(chatter)); } // Append point spread function information if (m_psf != NULL) { result.append("\n"+m_psf->print(chatter)); } // EXPLICIT: Append Npred cache information if (chatter >= EXPLICIT) { if (!m_npred_names.empty()) { for (int i = 0; i < m_npred_names.size(); ++i) { result.append("\n"+gammalib::parformat("Npred cache " + gammalib::str(i))); result.append(m_npred_names[i]+", "); result.append(m_npred_energies[i].print()+", "); result.append(m_npred_times[i].print()+" = "); result.append(gammalib::str(m_npred_values[i])); } } } // endif: chatter was explicit } // endif: chatter was not silent // Return result return result; } /*========================================================================== = = = Model type dependent CTA response methods = = = ==========================================================================*/ /***********************************************************************//** * @brief Return IRF value for radial source model * * @param[in] event Observed event. * @param[in] source Source. * @param[in] obs Observation. * * @exception GCTAException::bad_model_type * Model is not a radial model. * * Integrates the product of the model and the IRF over the true photon * arrival direction using * * \f[ * \int_{\rho_{\rm min}}^{\rho_{\rm max}} * \sin \rho \times S_{\rm p}(\rho | E, t) \times * \int_{\omega_{\rm min}}^{\omega_{\rm max}} * IRF(\rho, \omega) d\omega d\rho * \f] * * where * - \f$S_{\rm p}(\rho | E, t)\f$ is the radial model, * - \f$IRF(\rho, \omega)\f$ is the IRF * - \f$\rho\f$ is the distance from the model centre, and * - \f$\omega\f$ is the azimuth angle is the position angle with respect to * the connecting line between the model centre and the observed photon * arrival direction. * * The integration is performed in the coordinate system of the source * model spanned by \f$\rho\f$ and \f$\omega\f$ which allows to benefit * from the symmetry of the source model. * * The source centre is located at \f$\vec{m}\f$, and a spherical system * is defined around this location with \f$(\omega,\rho)\f$ being the * azimuth and zenith angles, respectively. \f$\omega=0\f$ is defined * by the direction that connects the source centre \f$\vec{m}\f$ to the * measured photon direction \f$\vec{p'}\f$, and \f$\omega\f$ increases * counterclockwise. * * Note that this method approximates the true theta angle (angle between * incident photon and pointing direction) by the measured theta angle * (angle between the measured photon arrival direction and the pointing * direction). Given the slow variation of the PSF shape over the field of * view, this approximation should be fine. It helps in fact a lot in * speeding up the computations. ***************************************************************************/ double GCTAResponse::irf_radial(const GEvent& event, const GSource& source, const GObservation& obs) const { // Retrieve CTA pointing const GCTAPointing& pnt = retrieve_pnt(G_IRF_RADIAL, obs); const GCTAInstDir& dir = retrieve_dir(G_IRF_RADIAL, event); // Get pointer on radial model const GModelSpatialRadial* model = dynamic_cast(source.model()); if (model == NULL) { throw GCTAException::bad_model_type(G_IRF_RADIAL); } // Get event attributes //const GSkyDir& obsDir = dir->dir(); const GEnergy& obsEng = event.energy(); // Get source attributes const GSkyDir& centre = model->dir(); const GEnergy& srcEng = source.energy(); const GTime& srcTime = source.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Determine angular distance between measured photon direction and model // centre [radians] double zeta = centre.dist(dir.dir()); // Determine angular distance between measured photon direction and // pointing direction [radians] double eta = pnt.dir().dist(dir.dir()); // Determine angular distance between model centre and pointing direction // [radians] double lambda = centre.dist(pnt.dir()); // Compute azimuth angle of pointing in model system [radians] // Will be comprised in interval [0,pi] double omega0 = 0.0; double denom = std::sin(lambda) * std::sin(zeta); if (denom != 0.0) { double arg = (std::cos(eta) - std::cos(lambda) * std::cos(zeta))/denom; omega0 = gammalib::acos(arg); } // Get log10(E/TeV) of true and measured photon energies double srcLogEng = srcEng.log10TeV(); double obsLogEng = obsEng.log10TeV(); // Assign the observed theta angle (eta) as the true theta angle // between the source and the pointing directions. This is a (not // too bad) approximation which helps to speed up computations. // If we want to do this correctly, however, we would need to move // the psf_dummy_sigma down to the integration kernel, and we would // need to make sure that psf_delta_max really gives the absolute // maximum (this is certainly less critical) double theta = eta; double phi = 0.0; //TODO: Implement IRF Phi dependence // Get maximum PSF and source radius in radians. double delta_max = psf_delta_max(theta, phi, zenith, azimuth, srcLogEng); double src_max = model->theta_max(); // Set radial model zenith angle range double rho_min = (zeta > delta_max) ? zeta - delta_max : 0.0; double rho_max = zeta + delta_max; if (rho_max > src_max) { rho_max = src_max; } // Initialise IRF value double irf = 0.0; // Perform zenith angle integration if interval is valid if (rho_max > rho_min) { // Setup integration kernel cta_irf_radial_kern_rho integrand(*this, *model, zenith, azimuth, srcEng, srcTime, srcLogEng, obsLogEng, zeta, lambda, omega0, delta_max); // Integrate over zenith angle GIntegral integral(&integrand); integral.eps(m_eps); irf = integral.romb(rho_min, rho_max); // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(irf) || gammalib::is_infinite(irf)) { std::cout << "*** ERROR: GCTAResponse::irf_radial:"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", rho_min=" << rho_min; std::cout << ", rho_max=" << rho_max; std::cout << ", omega0=" << omega0 << ")"; std::cout << std::endl; } #endif // Apply deadtime correction irf *= obs.deadc(srcTime); } // Compile option: Show integration results #if defined(G_DEBUG_IRF_RADIAL) std::cout << "GCTAResponse::irf_radial:"; std::cout << " rho_min=" << rho_min; std::cout << " rho_max=" << rho_max; std::cout << " irf=" << irf << std::endl; #endif // Return IRF value return irf; } /***********************************************************************//** * @brief Return IRF value for elliptical source model * * @param[in] event Observed event. * @param[in] source Source. * @param[in] obs Observation. * * @exception GCTAException::bad_instdir_type * Instrument direction is not a valid CTA instrument direction. * @exception GCTAException::bad_model_type * Model is not an elliptical model. * * Integrates the product of the model and the IRF over the true photon * arrival direction using * * \f[ * \int_{\rho_{\rm min}}^{\rho_{\rm max}} * \sin \rho \times * \int_{\omega_{\rm min}}^{\omega_{\rm max}} * S_{\rm p}(\rho, \omega | E, t) \, IRF(\rho, \omega) d\omega d\rho * \f] * * where * - \f$S_{\rm p}(\rho, \omega | E, t)\f$ is the radial model, * - \f$IRF(\rho, \omega)\f$ is the IRF * - \f$\rho\f$ is the distance from the model centre, and * - \f$\omega\f$ is the azimuth angle is the position angle with respect to * the connecting line between the model centre and the observed photon * arrival direction. * * The source model centre is located at \f$\vec{m}\f$, and a spherical * coordinate system is defined around this location with \f$(\rho,\omega)\f$ * being the zenith and azimuth angles, respectively. The azimuth angle * \f$(\omega)\f$ is counted counterclockwise from the vector that runs from * the model centre \f$\vec{m}\f$ to the measured photon direction * \f$\vec{p'}\f$. ***************************************************************************/ double GCTAResponse::irf_elliptical(const GEvent& event, const GSource& source, const GObservation& obs) const { // Retrieve CTA pointing const GCTAPointing& pnt = retrieve_pnt(G_IRF_ELLIPTICAL, obs); const GCTAInstDir& dir = retrieve_dir(G_IRF_ELLIPTICAL, event); // Get pointer on elliptical model const GModelSpatialElliptical* model = dynamic_cast(source.model()); if (model == NULL) { throw GCTAException::bad_model_type(G_IRF_ELLIPTICAL); } // Get event attributes (measured photon) const GSkyDir& obsDir = dir.dir(); const GEnergy& obsEng = event.energy(); // Get source attributes const GSkyDir& centre = model->dir(); const GEnergy& srcEng = source.energy(); const GTime& srcTime = source.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Determine angular distance between observed photon direction and model // centre and position angle of observed photon direction seen from the // model centre [radians] double zeta = centre.dist(obsDir); double obsOmega = centre.posang(obsDir); // Determine angular distance between measured photon direction and // pointing direction [radians] double eta = pnt.dir().dist(obsDir); // Determine angular distance between model centre and pointing direction // [radians] double lambda = centre.dist(pnt.dir()); // Compute azimuth angle of pointing in model coordinate system [radians] // This azimuth angle is comprised in the interval [0,pi], and defines // the zero point of the model coordinate system. double omega0 = 0.0; double denom = std::sin(lambda) * std::sin(zeta); if (denom != 0.0) { double arg = (std::cos(eta) - std::cos(lambda) * std::cos(zeta))/denom; omega0 = gammalib::acos(arg); } // Get log10(E/TeV) of true and measured photon energies double srcLogEng = srcEng.log10TeV(); double obsLogEng = obsEng.log10TeV(); // Get maximum PSF radius [radians]. We assign here the measured theta // angle (eta) as the true theta angle between the source and the pointing // directions. As we only use the angle to determine the maximum PSF size, // this should be sufficient. double theta = eta; double phi = 0.0; //TODO: Implement IRF Phi dependence double delta_max = psf_delta_max(theta, phi, zenith, azimuth, srcLogEng); // Get maximum source model radius [radians] double src_max = model->theta_max(); // Set zenith angle integration range for elliptical model double rho_min = (zeta > delta_max) ? zeta - delta_max : 0.0; double rho_max = zeta + delta_max; if (rho_max > src_max) { rho_max = src_max; } // Initialise IRF value double irf = 0.0; // Perform zenith angle integration if interval is valid if (rho_max > rho_min) { // Setup integration kernel cta_irf_elliptical_kern_rho integrand(*this, *model, zenith, azimuth, srcEng, srcTime, srcLogEng, obsLogEng, zeta, lambda, obsOmega, omega0, delta_max); // Integrate over zenith angle GIntegral integral(&integrand); integral.eps(m_eps); irf = integral.romb(rho_min, rho_max); // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(irf) || gammalib::is_infinite(irf)) { std::cout << "*** ERROR: GCTAResponse::irf_elliptical:"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", rho_min=" << rho_min; std::cout << ", rho_max=" << rho_max; std::cout << ", omega0=" << omega0 << ")"; std::cout << std::endl; } #endif // Apply deadtime correction irf *= obs.deadc(srcTime); } // Compile option: Show integration results #if defined(G_DEBUG_IRF_ELLIPTICAL) std::cout << "GCTAResponse::irf_elliptical:"; std::cout << " rho_min=" << rho_min; std::cout << " rho_max=" << rho_max; std::cout << " irf=" << irf << std::endl; #endif // Return IRF value return irf; } /***********************************************************************//** * @brief Return value of diffuse source instrument response function * * @param[in] event Observed event. * @param[in] source Source. * @param[in] obs Observation. * * Integrates the product of the model and the IRF over the true photon * arrival direction using * * \f[ * \int_{0}^{\theta_{\rm max}} * \sin \theta \times PSF(\theta) * \int_{0}^{2\pi} * S_{\rm p}(\theta, \phi | E, t) \, * Aeff(\theta, \phi) \, * Edisp(\theta, \phi) d\phi * \f] * * where * - \f$S_{\rm p}(\theta, \phi | E, t)\f$ is the diffuse model, * - \f$PSF(\theta)\f$ is the azimuthally symmetric Point Spread Function, * - \f$Aeff(\theta, \phi)\f$ is the effective area, * - \f$Edisp(\theta, \phi)\f$ is the energy dispersion, * - \f$\theta\f$ is the distance from the PSF centre, and * - \f$\phi\f$ is the azimuth angle. * * The integration is performed in the reference of the observed arrival * direction. Integration is done first over the azimuth angle \f$\phi\f$ and * then over the offset angle \f$\theta\f$. * * The integration kernels for this method are implemented by the response * helper classes cta_irf_diffuse_kern_theta and cta_irf_diffuse_kern_phi. ***************************************************************************/ double GCTAResponse::irf_diffuse(const GEvent& event, const GSource& source, const GObservation& obs) const { // Initialise IRF value bool has_irf = false; double irf = 0.0; // Retrieve CTA pointing const GCTAPointing& pnt = retrieve_pnt(G_IRF_DIFFUSE, obs); // Try getting the IRF value from cache #if defined(G_USE_IRF_CACHE) const GCTAEventList* list = dynamic_cast(obs.events()); const GCTAEventAtom* atom = dynamic_cast(&event); if (list != NULL && atom != NULL) { irf = list->irf_cache(source.name(), atom->index()); if (irf >= 0.0) { has_irf = true; #if defined(G_DEBUG_IRF_DIFFUSE) std::cout << "GCTAResponse::irf_diffuse:"; std::cout << " cached irf=" << irf << std::endl; #endif } else { irf = 0.0; } } #endif // Continue only if we have no IRF value if (!has_irf) { // Get CTA instrument direction const GCTAInstDir& dir = retrieve_dir(G_IRF_ELLIPTICAL, event); // Get pointer on spatial model const GModelSpatial* model = dynamic_cast(source.model()); if (model == NULL) { throw GCTAException::bad_model_type(G_IRF_DIFFUSE); } // Get event attributes //const GSkyDir& obsDir = dir.dir(); const GEnergy& obsEng = event.energy(); // Get source attributes const GEnergy& srcEng = source.energy(); const GTime& srcTime = source.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Determine angular distance between measured photon direction and // pointing direction [radians] double eta = pnt.dir().dist(dir.dir()); // Get log10(E/TeV) of true and measured photon energies double srcLogEng = srcEng.log10TeV(); double obsLogEng = obsEng.log10TeV(); // Assign the observed theta angle (eta) as the true theta angle // between the source and the pointing directions. This is a (not // too bad) approximation which helps to speed up computations. // If we want to do this correctly, however, we would need to move // the psf_dummy_sigma down to the integration kernel, and we would // need to make sure that psf_delta_max really gives the absolute // maximum (this is certainly less critical) double theta = eta; double phi = 0.0; //TODO: Implement Phi dependence // Get maximum PSF radius in radians double delta_max = psf_delta_max(theta, phi, zenith, azimuth, srcLogEng); // Perform zenith angle integration if interval is valid if (delta_max > 0.0) { // Compute rotation matrix to convert from coordinates (theta,phi) // in the reference frame of the observed arrival direction into // celestial coordinates GMatrix ry; GMatrix rz; ry.eulery(dir.dir().dec_deg() - 90.0); rz.eulerz(-dir.dir().ra_deg()); GMatrix rot = (ry * rz).transpose(); // Setup integration kernel cta_irf_diffuse_kern_theta integrand(*this, *model, theta, phi, zenith, azimuth, srcEng, srcTime, srcLogEng, obsLogEng, rot, eta); // Integrate over zenith angle GIntegral integral(&integrand); integral.eps(1.0e-4); irf = integral.romb(0.0, delta_max); // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(irf) || gammalib::is_infinite(irf)) { std::cout << "*** ERROR: GCTAResponse::irf_diffuse:"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", delta_max=" << delta_max << ")"; std::cout << std::endl; } #endif } // Apply deadtime correction irf *= obs.deadc(srcTime); // Put IRF value in cache #if defined(G_USE_IRF_CACHE) if (list != NULL && atom != NULL) { list->irf_cache(source.name(), atom->index(), irf); } #endif // Compile option: Show integration results #if defined(G_DEBUG_IRF_DIFFUSE) std::cout << "GCTAResponse::irf_diffuse:"; std::cout << " srcLogEng=" << srcLogEng; std::cout << " obsLogEng=" << obsLogEng; std::cout << " eta=" << eta; std::cout << " delta_max=" << delta_max; std::cout << " irf=" << irf << std::endl; #endif } // endif: has no IRF // Return IRF value return irf; } /***********************************************************************//** * @brief Return spatial integral of radial source model over ROI * * @param[in] source Source. * @param[in] obs Observation. * * @exception GCTAException::bad_model_type * Model is not a radial model. * * Integrates the product of the radial model and Npred over the Region Of * Interest using * * \f[ * \int_{\rho_{\rm min}}^{\rho_{\rm max}} * \sin \rho \times S_{\rm p}(\rho | E, t) \times * \int_{\omega_{\rm min}}^{\omega_{\rm max}} * N_{\rm pred}(\rho, \omega) d\omega * d\rho * \f] * * where * - \f$S_{\rm p}(\rho | E, t)\f$ is the radial model, * - \f$N_{\rm pred}(\rho,\omega)\f$ is the data space integral of the * Instrument Response Function for a point spread function over the * Region Of Interest, * - \f$\rho\f$ is the distance from the model centre, and * - \f$\omega\f$ is the azimuth angle is the position angle with respect to * the connecting line between the model centre and the observed photon * arrival direction. * * The integration is performed in a spherical coordinate system that is * centred on the source model centre \f$\vec{m}\f$, with \f$(\rho,\omega)\f$ * being the zenith and azimuth angles, respectively. * * The zenith angle integration range \f$[\rho_{\rm min}, \rho_{\rm max}\f$ * and azimuth angle integration range * \f$[\omega_{\rm min}, \omega_{\rm max}\f$ * are adjusted so that only coordinates within the circular region of * interest will be considered. * * Note that we estimate the integration radius based on the size of the * onaxis PSF. This should be fine as long as the offaxis PSF is not * considerably larger than the onaxis PSF. We should verify this, however. * * @todo Verify that offaxis PSF is not considerably larger than onaxis * PSF. ***************************************************************************/ double GCTAResponse::npred_radial(const GSource& source, const GObservation& obs) const { // Initialise Npred value double npred = 0.0; // Retrieve CTA observation, ROI and pointing const GCTAObservation& cta = retrieve_obs(G_NPRED_RADIAL, obs); const GCTARoi& roi = retrieve_roi(G_NPRED_RADIAL, obs); const GCTAPointing& pnt = cta.pointing(); // Get pointer on radial model const GModelSpatialRadial* model = dynamic_cast(source.model()); if (model == NULL) { throw GCTAException::bad_model_type(G_NPRED_RADIAL); } // Get source attributes const GSkyDir& centre = model->dir(); const GEnergy& srcEng = source.energy(); const GTime& srcTime = source.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Get log10(E/TeV) of true photon energy double srcLogEng = srcEng.log10TeV(); // Get maximum PSF radius (radians). We do this for the onaxis PSF only, // as this allows us doing this computation in the outer loop. This // should be sufficient here, unless the offaxis PSF becomes much worse // than the onaxis PSF. In this case, we may add a safety factor here // to make sure we encompass the entire PSF. double psf_max_radius = psf_delta_max(0.0, 0.0, zenith, azimuth, srcLogEng); // Extract ROI radius (radians) double roi_radius = roi.radius() * gammalib::deg2rad; // Compute distance between ROI and model centre (radians) double roi_model_distance = roi.centre().dir().dist(centre); // Compute the ROI radius plus maximum PSF radius (radians). Any photon // coming from beyond this radius will not make it in the dataspace and // thus can be neglected. double roi_psf_radius = roi_radius + psf_max_radius; // Set offset angle integration range. We take here the ROI+PSF into // account to make no integrations beyond the point where the // contribution drops to zero. double rho_min = (roi_model_distance > roi_psf_radius) ? roi_model_distance - roi_psf_radius: 0.0; double rho_max = model->theta_max(); // Perform offset angle integration only if interval is valid if (rho_max > rho_min) { // Compute rotation matrix to convert from native model coordinates, // given by (rho,omega), into celestial coordinates. GMatrix ry; GMatrix rz; ry.eulery(model->dec() - 90.0); rz.eulerz(-model->ra()); GMatrix rot = (ry * rz).transpose(); // Compute position angle of ROI centre with respect to model // centre (radians) double omega0 = centre.posang(roi.centre().dir()); // Setup integration kernel cta_npred_radial_kern_rho integrand(*this, *model, source.energy(), source.time(), cta, rot, roi_model_distance, roi_psf_radius, omega0); // Integrate over theta GIntegral integral(&integrand); npred = integral.romb(rho_min, rho_max); // Apply deadtime correction npred *= obs.deadc(srcTime); // Compile option: Show integration results #if defined(G_DEBUG_NPRED_RADIAL) std::cout << "GCTAResponse::npred_radial:"; std::cout << " rho_min=" << rho_min; std::cout << " rho_max=" << rho_max; std::cout << " npred=" << npred << std::endl; #endif } // endif: offset angle range was valid // Debug: Check for NaN #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(npred) || gammalib::is_infinite(npred)) { std::cout << "*** ERROR: GCTAResponse::npred_radial:"; std::cout << " NaN/Inf encountered"; std::cout << " (npred=" << npred; std::cout << ", rho_min=" << rho_min; std::cout << ", rho_max=" << rho_max; std::cout << ")" << std::endl; } #endif // Return Npred return npred; } /***********************************************************************//** * @brief Return spatial integral of elliptical source model over ROI * * @param[in] source Source. * @param[in] obs Observation. * * @exception GCTAException::bad_model_type * Model is not an elliptical model. * * Integrates the product of the elliptical model and Npred over the Region * Of Interest using * * \f[ * \int_{\rho_{\rm min}}^{\rho_{\rm max}} * \sin \rho \times * \int_{\omega_{\rm min}}^{\omega_{\rm max}} * S_{\rm p}(\rho,\omega | E, t) \, * N_{\rm pred}(\rho,\omega) d\omega * d\rho * \f] * * where * - \f$S_{\rm p}(\rho,\omega | E, t)\f$ is the elliptical model, * - \f$N_{\rm pred}(\rho,\omega)\f$ is the data space integral of the * Instrument Response Function for a point spread function over the * Region Of Interest, * - \f$\rho\f$ is the distance from the model centre, and * - \f$\omega\f$ is the azimuth angle is the position angle with respect to * the connecting line between the model centre and the observed photon * arrival direction. * * The integration is performed in a spherical coordinate system that is * centred on the source model centre \f$\vec{m}\f$, with \f$(\rho,\omega)\f$ * being the zenith and azimuth angles, respectively. * * The zenith angle integration range \f$[\rho_{\rm min}, \rho_{\rm max}\f$ * and azimuth angle integration range * \f$[\omega_{\rm min}, \omega_{\rm max}\f$ * are adjusted so that only coordinates within the circular region of * interest will be considered. * * Note that we estimate the integration radius based on the size of the * onaxis PSF. This should be fine as long as the offaxis PSF is not * considerably larger than the onaxis PSF. We should verify this, however. * * @todo Verify that offaxis PSF is not considerably larger than onaxis * PSF. ***************************************************************************/ double GCTAResponse::npred_elliptical(const GSource& source, const GObservation& obs) const { // Initialise Npred value double npred = 0.0; // Retrieve CTA observation, ROI and pointing const GCTAObservation& cta = retrieve_obs(G_NPRED_ELLIPTICAL, obs); const GCTARoi& roi = retrieve_roi(G_NPRED_ELLIPTICAL, obs); const GCTAPointing& pnt = cta.pointing(); // Get pointer on elliptical model const GModelSpatialElliptical* model = dynamic_cast(source.model()); if (model == NULL) { throw GCTAException::bad_model_type(G_NPRED_ELLIPTICAL); } // Get source attributes const GSkyDir& centre = model->dir(); const GEnergy& srcEng = source.energy(); const GTime& srcTime = source.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Get log10(E/TeV) of true photon energy double srcLogEng = srcEng.log10TeV(); // Get maximum PSF radius (radians). We do this for the onaxis PSF only, // as this allows us doing this computation in the outer loop. This // should be sufficient here, unless the offaxis PSF becomes much worse // than the onaxis PSF. In this case, we may add a safety factor here // to make sure we encompass the entire PSF. double psf_max_radius = psf_delta_max(0.0, 0.0, zenith, azimuth, srcLogEng); // Extract ROI radius (radians) double roi_radius = roi.radius() * gammalib::deg2rad; // Compute distance between ROI and model centre (radians) double roi_model_distance = roi.centre().dir().dist(centre); // Compute the ROI radius plus maximum PSF radius (radians). Any photon // coming from beyond this radius will not make it in the dataspace and // thus can be neglected. double roi_psf_radius = roi_radius + psf_max_radius; // Set offset angle integration range. We take here the ROI+PSF into // account to make no integrations beyond the point where the // contribution drops to zero. double rho_min = (roi_model_distance > roi_psf_radius) ? roi_model_distance - roi_psf_radius: 0.0; double rho_max = model->theta_max(); // Perform offset angle integration only if interval is valid if (rho_max > rho_min) { // Compute rotation matrix to convert from native model coordinates, // given by (rho,omega), into celestial coordinates. GMatrix ry; GMatrix rz; ry.eulery(model->dec() - 90.0); rz.eulerz(-model->ra()); GMatrix rot = (ry * rz).transpose(); // Compute position angle of ROI centre with respect to model // centre (radians) double omega0 = centre.posang(roi.centre().dir()); // Setup integration kernel cta_npred_elliptical_kern_rho integrand(*this, *model, source.energy(), source.time(), cta, rot, roi_model_distance, roi_psf_radius, omega0); // Integrate over theta GIntegral integral(&integrand); npred = integral.romb(rho_min, rho_max); // Apply deadtime correction npred *= obs.deadc(srcTime); // Compile option: Show integration results #if defined(G_DEBUG_NPRED_ELLIPTICAL) std::cout << "GCTAResponse::npred_elliptical:"; std::cout << " rho_min=" << rho_min; std::cout << " rho_max=" << rho_max; std::cout << " npred=" << npred << std::endl; #endif } // endif: offset angle range was valid // Debug: Check for NaN #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(npred) || gammalib::is_infinite(npred)) { std::cout << "*** ERROR: GCTAResponse::npred_elliptical:"; std::cout << " NaN/Inf encountered"; std::cout << " (npred=" << npred; std::cout << ", rho_min=" << rho_min; std::cout << ", rho_max=" << rho_max; std::cout << ")" << std::endl; } #endif // Return Npred return npred; } /***********************************************************************//** * @brief Return spatial integral of diffuse source model over ROI * * @param[in] source Source. * @param[in] obs Observation. * * @exception GCTAException::bad_model_type * Model is not a radial model. * * Integrates the product of the diffuse model and Npred over the Region * Of Interest using * * \f[ * \int_{0}^{\theta_{\rm max}} * \sin \theta \times * \int_{0}^{2\pi} * S_{\rm p}(\theta, \phi | E, t) \, * N_{\rm pred}(\theta, \phi) d\phi * d\theta * \f] * * where * - \f$S_{\rm p}(\theta, \phi | E, t)\f$ is the diffuse model, * - \f$N_{\rm pred}(\theta, \phi)\f$ is the data space integral of the * Instrument Response Function for a point spread function over the * Region Of Interest in the reference frame of the diffuse source * model * - \f$\theta\f$ is the distance from the ROI centre, and * - \f$\phi\f$ is the azimuth angle. * * Note that the integration precision was adjusted trading-off between * computation time and computation precision. A value of 1e-4 was judged * appropriate. * * Note that we estimate the integration radius based on the size of the * onaxis PSF in this method. This should be fine as long as the offaxis * PSF is not considerably larger than the onaxis PSF. We should verify * this, however. * * @todo Verify that offaxis PSF is not considerably larger than onaxis PSF. ***************************************************************************/ double GCTAResponse::npred_diffuse(const GSource& source, const GObservation& obs) const { // Initialise Npred value double npred = 0.0; bool has_npred = false; // Build unique identifier std::string id = source.name() + "::" + obs.id(); // Check if Npred value is already in cache #if defined(G_USE_NPRED_CACHE) if (!m_npred_names.empty()) { // Search for unique identifier, and if found, recover Npred value // and break for (int i = 0; i < m_npred_names.size(); ++i) { if (m_npred_names[i] == id && m_npred_energies[i] == source.energy() && m_npred_times[i] == source.time()) { npred = m_npred_values[i]; has_npred = true; #if defined(G_DEBUG_NPRED_DIFFUSE) std::cout << "GCTAResponse::npred_diffuse:"; std::cout << " cache=" << i; std::cout << " npred=" << npred << std::endl; #endif break; } } } // endif: there were values in the Npred cache #endif // Continue only if no Npred cache value was found if (!has_npred) { // Retrieve CTA observation, ROI and pointing const GCTAObservation& cta = retrieve_obs(G_NPRED_DIFFUSE, obs); const GCTARoi& roi = retrieve_roi(G_NPRED_DIFFUSE, obs); const GCTAPointing& pnt = cta.pointing(); // Get pointer on spatial model const GModelSpatial* model = dynamic_cast(source.model()); if (model == NULL) { throw GCTAException::bad_model_type(G_NPRED_DIFFUSE); } // Get source attributes const GEnergy& srcEng = source.energy(); const GTime& srcTime = source.time(); // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Get log10(E/TeV) of true photon energy double srcLogEng = srcEng.log10TeV(); // Get maximum PSF radius (radians). We do this for the onaxis PSF only, // as this allows us doing this computation in the outer loop. This // should be sufficient here, unless the offaxis PSF becomes much worse // than the onaxis PSF. In this case, we may add a safety factor here // to make sure we encompass the entire PSF. double psf_max_radius = psf_delta_max(0.0, 0.0, zenith, azimuth, srcLogEng); // Extract ROI radius (radians) double roi_radius = roi.radius() * gammalib::deg2rad; // Compute the ROI radius plus maximum PSF radius (radians). Any photon // coming from beyond this radius will not make it in the dataspace and // thus can be neglected. double roi_psf_radius = roi_radius + psf_max_radius; // Perform offset angle integration only if interval is valid if (roi_psf_radius > 0.0) { // Compute rotation matrix to convert from native ROI coordinates, // given by (theta,phi), into celestial coordinates. GMatrix ry; GMatrix rz; ry.eulery(roi.centre().dir().dec_deg() - 90.0); rz.eulerz(-roi.centre().dir().ra_deg()); GMatrix rot = (ry * rz).transpose(); // Setup integration kernel cta_npred_diffuse_kern_theta integrand(*this, *model, source.energy(), source.time(), cta, rot); // Integrate over theta GIntegral integral(&integrand); integral.eps(1.0e-4); npred = integral.romb(0.0, roi_psf_radius); // Apply deadtime correction npred *= obs.deadc(srcTime); // Compile option: Show integration results #if defined(G_DEBUG_NPRED_DIFFUSE) std::cout << "GCTAResponse::npred_diffuse:"; std::cout << " roi_psf_radius=" << roi_psf_radius; std::cout << " npred=" << npred; std::cout << " id=" << id << std::endl; #endif } // endif: offset angle range was valid // Store result in Npred cache #if defined(G_USE_NPRED_CACHE) m_npred_names.push_back(id); m_npred_energies.push_back(source.energy()); m_npred_times.push_back(source.time()); m_npred_values.push_back(npred); #endif // Debug: Check for NaN #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(npred) || gammalib::is_infinite(npred)) { std::cout << "*** ERROR: GCTAResponse::npred_diffuse:"; std::cout << " NaN/Inf encountered"; std::cout << " (npred=" << npred; std::cout << ", roi_psf_radius=" << roi_psf_radius; std::cout << ")" << std::endl; } #endif } // endif: Npred computation required // Return Npred return npred; } /*========================================================================== = = = Low-level CTA response methods = = = ==========================================================================*/ /***********************************************************************//** * @brief Return effective area (in units of cm2) * * @param[in] theta Radial offset angle of photon in camera (radians). * @param[in] phi Polar angle of photon in camera (radians). * @param[in] zenith Zenith angle of telescope pointing (radians). * @param[in] azimuth Azimuth angle of telescope pointing (radians). * @param[in] srcLogEng Log10 of true photon energy (E/TeV). * @return Effective area in units fo cm2. * * Returns the effective area as function of the true photon position in the * camera system and the telescope pointing direction in the Earth system. * * If no effective area response is defined, 0.0 is returned. ***************************************************************************/ double GCTAResponse::aeff(const double& theta, const double& phi, const double& zenith, const double& azimuth, const double& srcLogEng) const { // Get effective area double aeff = (m_aeff != NULL) ? (*m_aeff)(srcLogEng, theta, phi, zenith, azimuth) : 0.0; // Return effective area return aeff; } /***********************************************************************//** * @brief Return point spread function (in units of sr^-1) * * @param[in] delta Angular separation between true and measured photon * directions (radians). * @param[in] theta Radial offset angle of photon in camera (radians). * @param[in] phi Polar angle of photon in camera (radians). * @param[in] zenith Zenith angle of telescope pointing (radians). * @param[in] azimuth Azimuth angle of telescope pointing (radians). * @param[in] srcLogEng Log10 of true photon energy (E/TeV). * * Returns the point spread function for a given offset angle as function * of the true photon position in the camera system and the telescope * pointing direction in the Earth system. * * If no point spread function is defined, 0.0 is returned. ***************************************************************************/ double GCTAResponse::psf(const double& delta, const double& theta, const double& phi, const double& zenith, const double& azimuth, const double& srcLogEng) const { // Compute PSF double psf = (m_psf != NULL) ? (*m_psf)(delta, srcLogEng, theta, phi, zenith, azimuth) : 0.0; // Return PSF return psf; } /***********************************************************************//** * @brief Return maximum angular separation (in radians) * * @param[in] theta Radial offset angle in camera (radians). * @param[in] phi Polar angle in camera (radians). * @param[in] zenith Zenith angle of telescope pointing (radians). * @param[in] azimuth Azimuth angle of telescope pointing (radians). * @param[in] srcLogEng Log10 of true photon energy (E/TeV). * * This method returns the maximum angular separation between true and * measured photon directions for which the PSF is non zero as function * of the true photon position in the camera system and the telescope * pointing direction in the Earth system. * * If no point spread function is defined, 0.0 is returned. ***************************************************************************/ double GCTAResponse::psf_delta_max(const double& theta, const double& phi, const double& zenith, const double& azimuth, const double& srcLogEng) const { // Compute PSF double delta_max = (m_psf != NULL) ? m_psf->delta_max(srcLogEng, theta, phi, zenith, azimuth) : 0.0; // Return PSF return delta_max; } /***********************************************************************//** * @brief Return energy dispersion (in units or MeV^-1) * * @param[in] obsLogEng Log10 of measured photon energy (E/TeV). * @param[in] theta Radial offset angle in camera (radians). * @param[in] phi Polar angle in camera (radians). * @param[in] zenith Zenith angle of telescope pointing (radians). * @param[in] azimuth Azimuth angle of telescope pointing (radians). * @param[in] srcLogEng Log10 of true photon energy (E/TeV). ***************************************************************************/ double GCTAResponse::edisp(const double& obsLogEng, const double& theta, const double& phi, const double& zenith, const double& azimuth, const double& srcLogEng) const { // Compute energy dispersion double edisp = (m_edisp != NULL) ? (*m_edisp)(obsLogEng, srcLogEng, theta, phi, zenith, azimuth) : 0.0; // Return energy dispersion return edisp; } /***********************************************************************//** * @brief Return result of PSF integration over ROI. * * @param[in] srcDir True photon direction. * @param[in] srcLogEng Log10 of true photon energy (E/TeV). * @param[in] srcTime True photon arrival time (not used). * @param[in] pnt CTA pointing. * @param[in] roi CTA region of interest. * * This method integrates the PSF over the circular region of interest (ROI). * Integration is done in a polar coordinate system centred on the PSF since * the PSF is assumed to be azimuthally symmetric. The polar integration is * done using the method npsf_kern_rad_azsym() that computes analytically * the arclength that is comprised within the ROI. * * Note that the integration is only performed when the PSF is spilling out * of the ROI border, otherwise the integral is simply 1. Numerical * integration is done using the standard Romberg method. The integration * boundaries are computed so that only the PSF section that falls in the ROI * is considered. * * @todo Enhance romb() integration method for small integration regions * (see comment about kluge below) * @todo Implement phi dependence in camera system ***************************************************************************/ double GCTAResponse::npsf(const GSkyDir& srcDir, const double& srcLogEng, const GTime& srcTime, const GCTAPointing& pnt, const GCTARoi& roi) const { // Declare result double value = 0.0; // Get pointing direction zenith angle and azimuth [radians] double zenith = pnt.zenith(); double azimuth = pnt.azimuth(); // Compute offset angle of source direction in camera system double theta = pnt.dir().dist(srcDir); // Compute azimuth angle of source direction in camera system double phi = 0.0; //TODO: Implement phi dependence // Extract relevant parameters from arguments double roi_radius = roi.radius() * gammalib::deg2rad; double roi_psf_distance = roi.centre().dir().dist(srcDir); double rmax = psf_delta_max(theta, phi, zenith, azimuth, srcLogEng); // If PSF is fully enclosed by the ROI then skip the numerical // integration and assume that the integral is 1.0 if (roi_psf_distance + rmax <= roi_radius) { value = 1.0; } // ... otherwise perform numerical integration else { // Compute minimum PSF integration radius double rmin = (roi_psf_distance > roi_radius) ? roi_psf_distance - roi_radius : 0.0; // Continue only if integration range is valid if (rmax > rmin) { // Setup integration kernel cta_npsf_kern_rad_azsym integrand(*this, roi_radius, roi_psf_distance, srcLogEng, theta, phi, zenith, azimuth); // Setup integration GIntegral integral(&integrand); integral.eps(m_eps); // Radially integrate PSF. In case that the radial integration // region is small, we do the integration using a simple // trapezoidal rule. This is a kluge to prevent convergence // problems in the romb() method for small integration intervals. // Ideally, the romb() method should be enhanced to handle this // case automatically. The kluge threshold was fixed manually! if (rmax-rmin < 1.0e-12) { value = integral.trapzd(rmin, rmax); } else { value = integral.romb(rmin, rmax); } // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(value) || gammalib::is_infinite(value)) { std::cout << "*** ERROR: GCTAResponse::npsf:"; std::cout << " NaN/Inf encountered"; std::cout << " (value=" << value; std::cout << ", roi_radius=" << roi_radius; std::cout << ", roi_psf_distance=" << roi_psf_distance; //std::cout << ", sigma=" << sigma; std::cout << ", r=[" << rmin << "," << rmax << "])"; std::cout << std::endl; } #endif } // endif: integration range was valid } // endelse: numerical integration required // Return integrated PSF return value; } /***********************************************************************//** * @brief Return result of energy dispersion integral over energy range * * @param[in] srcDir True photon direction. * @param[in] srcEng True energy of photon. * @param[in] srcTime True photon arrival time. * @param[in] pnt CTA pointing. * @param[in] ebds Energy boundaries of data selection. * * @todo Implement integration over energy range. ***************************************************************************/ double GCTAResponse::nedisp(const GSkyDir& srcDir, const GEnergy& srcEng, const GTime& srcTime, const GCTAPointing& pnt, const GEbounds& ebds) const { // Dummy double nedisp = 1.0; // Return integral return nedisp; } /*========================================================================== = = = Private methods = = = ==========================================================================*/ /***********************************************************************//** * @brief Initialise class members * * We set the relative integration precision to 1e-5 as test images are * pretty smooth with this precision and computations are still reasonably * fast. * * The following timing was obtained on a 64 Bit machine (fermi) using the * script ./test_model for a disk, a Gaussian, and a shell model: * * Disk Gauss Shell * m_eps = 1e-3 : user 0m03.80s 0m13.41s 0m03.83s * m_eps = 1e-4 : user 0m03.85s 0m13.71s 0m04.68s * m_eps = 1e-5 : user 0m06.29s 0m23.22s 0m16.94s * m_eps = 1e-6 : user 0m12.65s 0m55.08s 1m32.52s ***************************************************************************/ void GCTAResponse::init_members(void) { // Initialise members m_caldb.clear(); m_rspname.clear(); m_rmffile.clear(); m_eps = 1.0e-5; // Precision for Romberg integration m_aeff = NULL; m_psf = NULL; m_edisp = NULL; // Initialise Npred cache m_npred_names.clear(); m_npred_energies.clear(); m_npred_times.clear(); m_npred_values.clear(); // Return return; } /***********************************************************************//** * @brief Copy class members * * @param[in] rsp Response to be copied ***************************************************************************/ void GCTAResponse::copy_members(const GCTAResponse& rsp) { // Copy members m_caldb = rsp.m_caldb; m_rspname = rsp.m_rspname; m_rmffile = rsp.m_rmffile; m_eps = rsp.m_eps; // Copy cache m_npred_names = rsp.m_npred_names; m_npred_energies = rsp.m_npred_energies; m_npred_times = rsp.m_npred_times; m_npred_values = rsp.m_npred_values; // Clone members m_aeff = (rsp.m_aeff != NULL) ? rsp.m_aeff->clone() : NULL; m_psf = (rsp.m_psf != NULL) ? rsp.m_psf->clone() : NULL; m_edisp = (rsp.m_edisp != NULL) ? rsp.m_edisp->clone() : NULL; // Return return; } /***********************************************************************//** * @brief Delete class members ***************************************************************************/ void GCTAResponse::free_members(void) { // Free memory if (m_aeff != NULL) delete m_aeff; if (m_psf != NULL) delete m_psf; if (m_edisp != NULL) delete m_edisp; // Initialise pointers m_aeff = NULL; m_psf = NULL; m_edisp = NULL; // Return return; } /***********************************************************************//** * @brief Retrieve CTA observation from generic observation * * @param[in] origin Method asking for pointer retrieval. * @param[in] obs Generic observation. * * @exception GException::invalid_argument * Observation @p obs is not a CTA observations. * * Dynamically casts generic observation into a CTA observation. If the * generic observation is not a CTA observation, an exception is thrown. ***************************************************************************/ const GCTAObservation& GCTAResponse::retrieve_obs(const std::string& origin, const GObservation& obs) const { // Get pointer on CTA observation const GCTAObservation* cta = dynamic_cast(&obs); // If pointer is not valid then throw an exception if (cta == NULL) { std::string msg = "Specified observation is not a CTA observation.\n" "Please specify a CTA observation when calling" " this method."; throw GException::invalid_argument(origin, msg); } // Return reference return *cta; } /***********************************************************************//** * @brief Retrieve CTA pointing from generic observation * * @param[in] origin Method asking for pointer retrieval. * @param[in] obs Generic observation. * * Extract CTA pointing from a CTA observation. ***************************************************************************/ const GCTAPointing& GCTAResponse::retrieve_pnt(const std::string& origin, const GObservation& obs) const { // Retrieve CTA observation and pointing const GCTAObservation& cta = retrieve_obs(origin, obs); // Return CTA pointing return (cta.pointing()); } /***********************************************************************//** * @brief Retrieve CTA ROI from generic observation * * @param[in] origin Method asking for pointer retrieval. * @param[in] obs Generic observation. * * @exception GException::invalid_argument * Observation @p obs does not contain a CTA event list. * * Extract CTA Region of Interest from a CTA observation. ***************************************************************************/ const GCTARoi& GCTAResponse::retrieve_roi(const std::string& origin, const GObservation& obs) const { // Retrieve CTA observation const GCTAObservation& cta = retrieve_obs(origin, obs); // Get pointer on CTA events list const GCTAEventList* events = dynamic_cast(cta.events()); // If pointer is not valid then throw an exception if (events == NULL) { std::string msg = "Specified observation does not contain a CTA event" " list.\n" "Please specify a CTA observation containing a CTA" " event list when calling this method."; throw GException::invalid_argument(origin, msg); } // Return CTA ROI return (events->roi()); } /***********************************************************************//** * @brief Retrieve CTA instrument direction from generic event * * @param[in] origin Method asking for pointer retrieval. * @param[in] event Generic event. * * @exception GException::invalid_argument * @p event does not contain a CTA instrument direction. * * Extract CTA Instrument Direction from an event. ***************************************************************************/ const GCTAInstDir& GCTAResponse::retrieve_dir(const std::string& origin, const GEvent& event) const { // Get pointer on CTA instrument direction const GCTAInstDir* dir = dynamic_cast(&(event.dir())); // If pointer is not valid then throw an exception if (dir == NULL) { std::string msg = "Specified event is not a CTA event.\n" "Please specify a CTA event when calling this method."; throw GException::invalid_argument(origin, msg); } // Return reference return *dir; }