/***********************************************************************//** * @brief Temporally integrates spatially and spectrally integrated Npred * kernel * * @param[in] model Gamma-ray source model. * * @exception GException::gti_invalid * Good Time Interval is invalid. * * Computes * \f[N_{\rm pred} = \int_{\rm GTI} \int_{E_{\rm bounds}} \int_{\rm ROI} * S(\vec{p}, E, t) PSF(\vec{p'}, E', t' | \vec{d}, \vec{p}, E, t) \, * {\rm d}\vec{p'} {\rm d}E' {\rm d}t'\f] * where * \f$S(\vec{p}, E, t)\f$ is the source model, * \f$PSF(\vec{p'}, E', t' | \vec{d}, \vec{p}, E, t)\f$ is the point * spread function, * \f$\vec{p'}\f$ is the measured photon direction, * \f$E'\f$ is the measured photon energy, * \f$t'\f$ is the measured photon arrival time, * \f$\vec{p}\f$ is the true photon arrival direction, * \f$E\f$ is the true photon energy, * \f$t\f$ is the true photon arrival time, and * \f$d\f$ is the instrument pointing. * * \f${\rm GTI}\f$ are the Good Time Intervals that are stored in the * GObservation::m_gti member. * Note that MET is used for the time integration interval. This, however, * is no specialisation since npred_grad_kern_spec::eval() converts the * argument back in a GTime object by assuming that the argument is in MET, * hence the correct time system will be used at the end by the method. ***************************************************************************/ double GObservation::npred_temp(const GModel& model) const { // Initialise result double result = 0.0; // Loop over GTIs for (int i = 0; i < events()->gti().size(); ++i) { // Set integration interval in seconds double tstart = events()->gti().tstart(i).secs(); double tstop = events()->gti().tstop(i).secs(); // Throw exception if time interval is not valid if (tstop <= tstart) { throw GException::gti_invalid(G_NPRED_TEMP, events()->gti().tstart(i), events()->gti().tstop(i)); } // Setup integration function GObservation::npred_temp_kern integrand(this, &model); GIntegral integral(&integrand); // Do Romberg integration result += integral.romb(tstart, tstop); } // endfor: looped over GTIs // Return result return result; }
/******************************************** * _:_ _:_ : : /:\ : : * * | : | * | : | = : / : \ : * * | : | | : | :/ : : : \: * * r:-1 0 1 x r:-2 -1 0 1 2 * * j=-1:j=0 j=-1:j=0 :j=2 * * j==section :j=1 * * section <= x <= section+1 * ********************************************/ Pol1Var Weighting( int order, int section ) { Pol1Var result("x"); /* need order+1 powers ! */ if ( order == 1 ) { if ( section == -1 or section == 0 ) result = "1"; else result = "0"; } else { Pol1Var tmp("x"); Pol1Var intoldvar("x"); Pol2Vars integrand("x,r"); integrand = Weighting( order-1, section-1 ).renameVariables("x","r"); result = integrand.integrate( "r", "x-1", toString(section) ); integrand = Weighting( order-1, section ).renameVariables("x","r"); result += integrand.integrate( "r", toString(section), toString(section+1) ); integrand = Weighting( order-1, section+1 ).renameVariables("x","r"); result += integrand.integrate( "r", toString(section+1), "x+1" ); } return result; }
/***********************************************************************//** * @brief Radial profile * * @param[in] theta Angular distance from DMBurkert centre (radians). * @return Profile value. ***************************************************************************/ double GModelSpatialRadialProfileDMBurkert::profile_value(const double& theta) const { // Update precompuation cache update(); // Initialize integral value double value = 0.0; // Set up integration limits double los_min = m_halo_distance.value() - m_mass_radius; double los_max = m_halo_distance.value() + m_mass_radius; // Handle case where observer is within halo mass radius if (los_min < 0.0) { los_min = 0.0; } // Set up integral halo_kernel_los integrand(m_scale_radius.value(), m_halo_distance.value(), theta, m_core_radius.value()); GIntegral integral(&integrand); // Compute value value = integral.romberg(los_min, los_max) * m_scale_density_squared; // Return value return value; }
Real ExtendedOrnsteinUhlenbeckProcess::expectation( Time t0, Real x0, Time dt) const { switch (discretization_) { case MidPoint: return ouProcess_->expectation(t0, x0, dt) + b_(t0+0.5*dt)*(1.0 - std::exp(-speed_*dt)); break; case Trapezodial: { const Time t = t0+dt; const Time u = t0; const Real bt = b_(t); const Real bu = b_(u); const Real ex = std::exp(-speed_*dt); return ouProcess_->expectation(t0, x0, dt) + bt-ex*bu - (bt-bu)/(speed_*dt)*(1-ex); } break; case GaussLobatto: return ouProcess_->expectation(t0, x0, dt) + speed_*std::exp(-speed_*(t0+dt)) * GaussLobattoIntegral(100000, intEps_)(integrand(b_, speed_), t0, t0+dt); break; default: QL_FAIL("unknown discretization scheme"); } }
/***********************************************************************//** * @brief Kernel for zenith angle Npred integration of background model * * @param[in] theta Offset angle from ROI centre (radians). * * Computes * * \f[ * K(\rho | E, t) = \sin \theta \times * \int_{\phi_{\rm min}}^{\phi_{\rm max}} * S_{\rm p}(\theta,\phi | E, t) \, * N_{\rm pred}(\theta,\phi) d\phi * \f] * * The azimuth angle integration range * \f$[\phi_{\rm min}, \phi_{\rm max}\f$ * is limited to an arc around the vector connecting the model centre to * the ROI centre. This limitation assures proper converges properly. ***************************************************************************/ double GCTAModelBackground::npred_roi_kern_theta::eval(const double& theta) { // Initialise value double value = 0.0; // Continue only if offset angle is positive if (theta > 0.0) { // Compute sine of offset angle double sin_theta = std::sin(theta); #if defined(G_NPRED_AROUND_ROI) double dphi = gammalib::pi; #else double dphi = 0.5 * gammalib::cta_roi_arclength(theta, m_dist, m_cosdist, m_sindist, m_roi, m_cosroi); #endif // Continue only if arc length is positive if (dphi > 0.0) { // Compute phi integration range double phi_min = m_omega0 - dphi; double phi_max = m_omega0 + dphi; // Setup phi integration kernel GCTAModelBackground::npred_roi_kern_phi integrand(m_model, m_obsEng, m_obsTime, m_rot, theta, sin_theta); // Integrate over phi GIntegral integral(&integrand); integral.eps(1e-3); value = integral.romb(phi_min, phi_max) * sin_theta; // Debug: Check for NaN #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(value) || gammalib::is_infinite(value)) { std::cout << "*** ERROR: GCTAModelBackground::npred_roi_kern_theta::eval"; std::cout << "(theta=" << theta << "):"; std::cout << " NaN/Inf encountered"; std::cout << " (value=" << value; std::cout << ", sin_theta=" << sin_theta; std::cout << ")" << std::endl; } #endif } // endif: arc length was positive } // endif: offset angle was positive // Return value return value; }
/***********************************************************************//** * @brief Returns model energy flux between [emin, emax] (units: erg/cm2/s) * * @param[in] emin Minimum photon energy. * @param[in] emax Maximum photon energy. * @return Energy flux (erg/cm2/s). * * Computes * * \f[ * \int_{\tt emin}^{\tt emax} S_{\rm E}(E | t) E \, dE * \f] * * where * - [@p emin, @p emax] is an energy interval, and * - \f$S_{\rm E}(E | t)\f$ is the spectral model (ph/cm2/s/MeV). * The integration is done numerically. ***************************************************************************/ double GModelSpectralExpPlaw::eflux(const GEnergy& emin, const GEnergy& emax) const { // Initialise flux double eflux = 0.0; // Compute only if integration range is valid if (emin < emax) { // Setup integration kernel eflux_kernel integrand(m_norm.value(), m_index.value(), m_pivot.value(), m_ecut.value()); GIntegral integral(&integrand); // Get integration boundaries in MeV double e_min = emin.MeV(); double e_max = emax.MeV(); // Perform integration eflux = integral.romberg(e_min, e_max); // Convert from MeV/cm2/s to erg/cm2/s eflux *= gammalib::MeV2erg; } // endif: integration range was valid // Return return eflux; }
/***********************************************************************//** * @brief Kernel for offset angle integration of background model * * @param[in] theta Offset angle from ROI centre (radians). * * Computes * * \f[ * K(\rho | E, t) = \sin \theta \times * \int_{0}^{2\pi} * B(\theta,\phi | E, t) d\phi * \f] * * where \f$B(\theta,\phi | E, t)\f$ is the background model for a specific * observed energy \f$E\f$ and time \f$t\f$. ***************************************************************************/ double GCTAModelIrfBackground::npred_roi_kern_theta::eval(const double& theta) { // Initialise value double value = 0.0; // Continue only if offset angle is positive if (theta > 0.0) { // Setup phi integration kernel GCTAModelIrfBackground::npred_roi_kern_phi integrand(m_bgd, m_logE, theta); // Integrate over phi GIntegral integral(&integrand); integral.eps(g_cta_inst_background_npred_phi_eps); value = integral.romberg(0.0, gammalib::twopi) * std::sin(theta); // Debug: Check for NaN #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(value) || gammalib::is_infinite(value)) { std::string origin = "GCTAModelIrfBackground::npred_roi_kern_theta::eval" "(" + gammalib::str(theta) + ")"; std::string message = " NaN/Inf encountered (value=" + gammalib::str(value) + ")"; gammalib::warning(origin, message); } #endif } // endif: offset angle was positive // Return value return value; }
/***********************************************************************//** * @brief Remove thetacut * * @param[in] rsp CTA response. * * Removes thetacut from Aeff values read from a FITS file. Note that this * method should only be called once directly after loading all response * components. ***************************************************************************/ void GCTAAeffArf::remove_thetacut(const GCTAResponse& rsp) { // Continue only if thetacut value has been set if (m_thetacut > 0.0) { // Get maximum integration radius double rmax = m_thetacut * gammalib::deg2rad; // Loop over Aeff array for (int i = 0; i < m_aeff.size(); ++i) { // Setup integration kernel for on-axis PSF cta_npsf_kern_rad_azsym integrand(rsp, rmax, 0.0, m_logE[i], 0.0, 0.0, 0.0, 0.0); // Setup integration GIntegral integral(&integrand); integral.eps(rsp.eps()); // Perform integration double fraction = integral.romb(0.0, rmax); // Set scale factor double scale = 1.0; if (fraction > 0.0) { scale /= fraction; #if defined(G_DEBUG_APPLY_THETACUT) std::cout << "GCTAAeffArf::apply_thetacut:"; std::cout << " logE=" << m_logE[i]; std::cout << " scale=" << scale; std::cout << " fraction=" << fraction; std::cout << std::endl; #endif } else { std::cout << "WARNING: GCTAAeffArf::apply_thetacut:"; std::cout << " Non-positive integral occured in"; std::cout << " PSF integration."; std::cout << std::endl; } // Apply scaling factor if (scale != 1.0) { m_aeff[i] *= scale; } } // endfor: looped over Aeff array } // endif: thetacut value was set // Return return; }
/***********************************************************************//** * @brief Integrates PSF for a specific set of parameters * * @param[in] energy Energy (MeV). * @param[in] index Parameter array index. * * Integrates PSF for a specific set of parameters. * * Compile option G_APPROXIMATE_PSF_INTEGRAL: * If defined, a numerical PSF integral is only performed for energies * < 120 MeV, while for larger energies the small angle approximation is * used. In not defined, a numerical PSF integral is performed for all * energies. * This option is kept for comparison with the Fermi/LAT ScienceTools who * select the integration method based on the true photon energy. As the * normalization is only performed once upon loading of the PSF, CPU time * is not really an issue here, and we can afford the more precise numerical * integration. Note that the uncertainties of the approximation at energies * near to 120 MeV reaches 0.1%. * * @todo Implement gcore and gtail checking ***************************************************************************/ double GLATPsfV3::integrate_psf(const double& energy, const int& index) { // Initialise integral double psf = 0.0; // Get energy scaling double scale = scale_factor(energy); // Get parameters double ncore(m_ncore[index]); double ntail(m_ntail[index]); double score(m_score[index] * scale); double stail(m_stail[index] * scale); double gcore(m_gcore[index]); double gtail(m_gtail[index]); // Make sure that gcore and gtail are not negative //if (gcore < 0 || gtail < 0) { //} // Do we need an exact integral? #if defined(G_APPROXIMATE_PSF_INTEGRAL) if (energy < 120) { #endif // Allocate integrand GLATPsfV3::base_integrand integrand(ncore, ntail, score, stail, gcore, gtail); // Allocate integral GIntegral integral(&integrand); // Integrate radially from 0 to 90 degrees psf = integral.romb(0.0, pihalf) * twopi; #if defined(G_APPROXIMATE_PSF_INTEGRAL) } // endif: exact integral was performed // No, so we use the small angle approximation else { // Compute arguments double rc = pihalf / score; double uc = 0.5 * rc * rc; double sc = twopi * score * score; double rt = pihalf / stail; double ut = 0.5 * rt * rt; double st = twopi * stail * stail; // Evaluate PSF integral (from 0 to 90 degrees) psf = ncore * (base_int(uc, gcore) * sc + base_int(ut, gtail) * st * ntail); } #endif // Return PSF integral return psf; }
/***********************************************************************//** * @brief Kernel for zenith angle Npred integration or radial model * * @param[in] theta Radial model zenith angle (radians). * * This method integrates a radial model for a given zenith angle theta over * all azimuth angles that fall within the ROI+PSF radius. The limitation to * an arc assures that the integration converges properly. ***************************************************************************/ double cta_npred_radial_kern_theta::eval(double theta) { // Initialise Npred value double npred = 0.0; // Compute half length of arc that lies within ROI+PSF radius (radians) double dphi = 0.5 * cta_roi_arclength(theta, m_dist, m_cos_dist, m_sin_dist, m_radius, m_cos_radius); // Continue only if arc length is positive if (dphi > 0.0) { // Compute phi integration range double phi_min = m_phi - dphi; double phi_max = m_phi + dphi; // Get radial model value double model = m_model->eval(theta); // Compute sine of offset angle double sin_theta = std::sin(theta); // Setup phi integration kernel cta_npred_radial_kern_phi integrand(m_rsp, m_srcEng, m_srcTime, m_obs, m_rot, theta, sin_theta); // Integrate over phi GIntegral integral(&integrand); npred = integral.romb(phi_min, phi_max) * sin_theta * model; // Debug: Check for NaN #if defined(G_NAN_CHECK) if (isnotanumber(npred) || isinfinite(npred)) { std::cout << "*** ERROR: cta_npred_radial_kern_theta::eval"; std::cout << "(theta=" << theta << "):"; std::cout << " NaN/Inf encountered"; std::cout << " (npred=" << npred; std::cout << ", model=" << model; std::cout << ", phi=[" << phi_min << "," << phi_max << "]"; std::cout << ", sin_theta=" << sin_theta; std::cout << ")" << std::endl; } #endif } // endif: arc length was positive // Return Npred return npred; }
Real calorimeterLSF::getLSF (Real deltaE1, Real deltaE2) { // Real answer = qags (deltaE2, deltaE1); // Real answer = qng (deltaE2, deltaE1); setEpsRel (1.e-3); // desired accuracy of integral over energy bin Real limit = 1.e-10 / itsETau; // below a certain threshold throw out the tail if (compare (integrand (deltaE1), limit) == -1) { if (compare (integrand (deltaE2), limit) == -1) { return 0.; } } Real answer = qag (deltaE2, deltaE1, 4); int status = getStatus (); if (status) { cout << deltaE1 << " " << deltaE2 << " " << answer << "\n"; cout << integrand (deltaE1) << " " << integrand (deltaE2) << "\n"; } return qags (deltaE2, deltaE1); }
/***********************************************************************//** * @brief Returns integral over radial model (in steradians) * * Computes * \f[\Omega = 2 \pi \int_0^{\pi} \sin \theta f(\theta) d\theta\f] * where * \f[f(\theta) = (1 + (\theta/c_0)^{c_1})^{-c_2/c_1}\f] * \f$\theta\f$ is the offset angle (in degrees), * \f$c_0\f$ is the width of the profile (width), * \f$c_1\f$ is the width of the central plateau (core), and * \f$c_2\f$ is the size of the tail (tail). * * The integration is performed numerically, and the upper integration bound * \f$\pi\f$ * is fixed to 10 degrees. ***************************************************************************/ double GCTAModelRadialProfile::omega(void) const { // Set constants const double offset_max_rad = 10.0 * deg2rad; // Allocate integrand GCTAModelRadialProfile::integrand integrand(this); // Allocate intergal GIntegral integral(&integrand); // Perform numerical integration double omega = integral.romb(0.0, offset_max_rad) * twopi; // Return integral return omega; }
/***********************************************************************//** * @brief Normalize PSF for all parameters * * Makes sure that PSF is normalized for all parameters. We assure this by * looping over all parameter nodes, integrating the PSF for each set of * parameters, and dividing the NCORE parameter by the integral. * * Compile option G_CHECK_PSF_NORM: * If defined, checks that the PSF is normalized correctly. ***************************************************************************/ void GLATPsfV3::normalize_psf(void) { // Loop over all energy bins for (int ie = 0; ie < m_rpsf_bins.nenergies(); ++ie) { // Extract energy value (in MeV) double energy = m_rpsf_bins.energy(ie); // Loop over all cos(theta) bins for (int ic = 0; ic < m_rpsf_bins.ncostheta(); ++ic) { // Get parameter index int index = m_rpsf_bins.index(ie, ic); // Integrate PSF double norm = integrate_psf(energy, index); // Normalize PSF m_ncore[index] /= norm; // Compile option: check PSF normalization #if defined(G_CHECK_PSF_NORM) double scale = scale_factor(energy); double ncore(m_ncore[index]); double ntail(m_ntail[index]); double score(m_score[index] * scale); double stail(m_stail[index] * scale); double gcore(m_gcore[index]); double gtail(m_gtail[index]); GLATPsfV3::base_integrand integrand(ncore, ntail, score, stail, gcore, gtail); GIntegral integral(&integrand); double sum = integral.romb(0.0, pihalf) * twopi; std::cout << "Energy=" << energy; std::cout << " cos(theta)=" << m_rpsf_bins.costheta_lo(ic); std::cout << " error=" << sum-1.0 << std::endl; #endif } // endfor: looped over cos(theta) } // endfor: looped over energies // Return return; }
/***********************************************************************//** * @brief Kernel for Npred offest angle integration of diffuse model * * @param[in] theta Offset angle with respect to ROI centre (radians). * * This method integrates a diffuse model for a given offset angle with * respect to the ROI centre over all azimuth angles (from 0 to 2pi). The * integration is only performed for positive offset angles, otherwise 0 is * returned. * * Integration is done using the Romberg algorithm. The integration kernel * is defined by the helper class cta_npred_diffuse_kern_phi. * * Note that the integration precision was adjusted trading-off between * computation time and computation precision. A value of 1e-4 was judged * appropriate. ***************************************************************************/ double cta_npred_diffuse_kern_theta::eval(double theta) { // Initialise Npred value double npred = 0.0; // Continue only if offset angle is positive if (theta > 0.0) { // Compute sine of offset angle double sin_theta = std::sin(theta); // Setup phi integration kernel cta_npred_diffuse_kern_phi integrand(m_rsp, m_model, m_srcEng, m_srcTime, m_obs, m_rot, theta, sin_theta); // Integrate over phi GIntegral integral(&integrand); integral.eps(1.0e-4); npred = integral.romb(0.0, twopi) * sin_theta; // Debug: Check for NaN #if defined(G_NAN_CHECK) if (isnotanumber(npred) || isinfinite(npred)) { std::cout << "*** ERROR: cta_npred_radial_kern_theta::eval"; std::cout << "(theta=" << theta << "):"; std::cout << " NaN/Inf encountered"; std::cout << " (npred=" << npred; std::cout << ", sin_theta=" << sin_theta; std::cout << ")" << std::endl; } #endif } // endif: offset angle was positive // Return Npred return npred; }
void Interval::checkFlux() const { if (_fluxIsReady) return; if (_isRadial) { // Integrate r*F RTimesF<FluxDensity> integrand(*_fluxDensityPtr); _flux = integ::int1d(integrand, _xLower, _xUpper, odd::RELATIVE_ERROR, odd::ABSOLUTE_ERROR); } else { // Integrate the input function _flux = integ::int1d(*_fluxDensityPtr, _xLower, _xUpper, odd::RELATIVE_ERROR, odd::ABSOLUTE_ERROR); } _fluxIsReady = true; }
void Interval::checkFlux() const { if (_fluxIsReady) return; if (_isRadial) { // Integrate r*F RTimesF<FluxDensity> integrand(*_fluxDensityPtr); _flux = integ::int1d(integrand, _xLower, _xUpper, _gsparams->integration_relerr, _gsparams->integration_abserr); } else { // Integrate the input function _flux = integ::int1d(*_fluxDensityPtr, _xLower, _xUpper, _gsparams->integration_relerr, _gsparams->integration_abserr); } _fluxIsReady = true; }
/***********************************************************************//** * @brief Returns integral over radial model (in steradians) * * Computes * \f[\Omega = 2 \pi \int_0^{\pi} \sin \theta f(\theta) d\theta\f] * where * \f[f(\theta) = \exp \left(-\frac{1}{2} * \left( \frac{\theta^2}{\sigma} \right)^2 \right)\f] * \f$\theta\f$ is the offset angle (in degrees), and * \f$\sigma\f$ is the Gaussian width (in degrees\f$^2\f$). * * The integration is performed numerically, and the upper integration bound * \f$\pi\f$ * is set to * \f$\sqrt(10 \sigma)\f$ * to reduce inaccuracies in the numerical integration. ***************************************************************************/ double GCTABackgroundPerfTable::solidangle(void) const { // Allocate integrand GCTABackgroundPerfTable::integrand integrand(sigma() * gammalib::deg2rad * gammalib::deg2rad); // Allocate intergal GIntegral integral(&integrand); // Set upper integration boundary double offset_max = std::sqrt(10.0*sigma()) * gammalib::deg2rad; if (offset_max > gammalib::pi) { offset_max = gammalib::pi; } // Perform numerical integration double solidangle = integral.romberg(0.0, offset_max) * gammalib::twopi; // Return integral return solidangle; }
/***********************************************************************//** * @brief Integrates spatially integrated Npred kernel spectrally * * @param[in] model Gamma-ray source model. * @param[in] obsTime Measured photon arrival time. * * @exception GException::erange_invalid * Energy range is invalid. * * Computes * \f[N'_{\rm pred} = \int_{E_{\rm bounds}} \int_{\rm ROI} * S(\vec{p}, E, t) PSF(\vec{p'}, E', t' | \vec{d}, \vec{p}, E, t) \, * {\rm d}\vec{p'} {\rm d}E'\f] * where * \f$S(\vec{p}, E, t)\f$ is the source model, * \f$PSF(\vec{p'}, E', t' | \vec{d}, \vec{p}, E, t)\f$ is the point * spread function, * \f$\vec{p'}\f$ is the measured photon direction, * \f$E'\f$ is the measured photon energy, * \f$t'\f$ is the measured photon arrival time, * \f$\vec{p}\f$ is the true photon arrival direction, * \f$E\f$ is the true photon energy, * \f$t\f$ is the true photon arrival time, and * \f$d\f$ is the instrument pointing. * * \f$E_{\rm bounds}\f$ are the energy boundaries that are stored in the * GObservation::m_ebounds member. * * @todo Loop also over energy boundaries (is more general; there is no * reason for not doing it). ***************************************************************************/ double GObservation::npred_spec(const GModel& model, const GTime& obsTime) const { // Set integration energy interval in MeV double emin = events()->ebounds().emin().MeV(); double emax = events()->ebounds().emax().MeV(); // Throw exception if energy range is not valid if (emax <= emin) { throw GException::erange_invalid(G_NPRED_SPEC, emin, emax); } // Setup integration function GObservation::npred_spec_kern integrand(this, &model, &obsTime); GIntegral integral(&integrand); // Do Romberg integration #if defined(G_LN_ENERGY_INT) emin = log(emin); emax = log(emax); #endif double result = integral.romb(emin, emax); // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (isnotanumber(result) || isinfinite(result)) { std::cout << "*** ERROR: GObservation::npred_spec:"; std::cout << " NaN/Inf encountered"; std::cout << " (result=" << result; std::cout << ", emin=" << emin; std::cout << ", emax=" << emax; std::cout << ")" << std::endl; } #endif // Return result return result; }
/***********************************************************************//** * @brief Kernel for radial model zenith angle integration of IRF * * @param[in] rho Zenith angle with respect to model centre [radians]. * * This method evaluates the kernel \f$K(\rho)\f$ for the zenith angle * integration * \f[\int_{\rho_{\rm min}}^{\rho_{\rm max}} K(\rho) d\rho\f] * of the product between model and IRF, where * \f[K(\rho) = \int_{\omega_{\rm min}}^{\omega_{\rm max}} M(\rho) * IRF(\rho, \omega) d\omega\f], * \f$M(\rho)\f$ is the azimuthally symmetric source model, and * \f$IRF(\rho, \omega)\f$ is the instrument response function. ***************************************************************************/ double cta_irf_radial_kern_rho::eval(double rho) { // Compute half length of arc that lies within PSF validity circle // (in radians) double domega = 0.5 * cta_roi_arclength(rho, m_zeta, m_cos_zeta, m_sin_zeta, m_delta_max, m_cos_delta_max); // Initialise result double irf = 0.0; // Continue only if arc length is positive if (domega > 0.0) { // Compute omega integration range double omega_min = -domega; double omega_max = +domega; // Evaluate sky model M(rho) double model = m_model->eval(rho); // Precompute cosine and sine terms for azimuthal integration double cos_rho = std::cos(rho); double sin_rho = std::sin(rho); double cos_psf = cos_rho*m_cos_zeta; double sin_psf = sin_rho*m_sin_zeta; double cos_ph = cos_rho*m_cos_lambda; double sin_ph = sin_rho*m_sin_lambda; // Setup integration kernel cta_irf_radial_kern_omega integrand(m_rsp, m_zenith, m_azimuth, m_srcLogEng, m_obsLogEng, m_zeta, m_lambda, m_omega0, rho, cos_psf, sin_psf, cos_ph, sin_ph); // Integrate over phi GIntegral integral(&integrand); integral.eps(m_rsp->eps()); irf = integral.romb(omega_min, omega_max) * model * sin_rho; // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (isnotanumber(irf) || isinfinite(irf)) { std::cout << "*** ERROR: cta_irf_radial_kern_rho"; std::cout << "(rho=" << rho << "):"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", domega=" << domega; std::cout << ", model=" << model; std::cout << ", sin_rho=" << sin_rho << ")"; std::cout << std::endl; } #endif } // endif: arc length was positive // Return result return irf; }
/***********************************************************************//** * @brief Integrate Psf over radial model * * @param[in] model Radial model. * @param[in] rho_obs Angle between model centre and measured photon direction * (radians). * @param[in] obsDir Observed event direction. * @param[in] srcEng True photon energy. * @param[in] srcTime True photon arrival time. * * Integrates the product of the spatial model and the point spread * function 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}} * {\rm Psf}(\rho, \omega) d\omega d\rho * \f] * * where * \f$S_{\rm p}(\rho | E, t)\f$ is the radial spatial model, * \f${\rm Psf}(\rho, \omega)\f$ is the point spread function, * \f$\rho\f$ is the radial distance from the model centre, and * \f$\omega\f$ is the position angle around the model centre measured * counterclockwise from the connecting line between the model centre and * the observed photon arrival direction. ***************************************************************************/ double GCTAResponseCube::psf_radial(const GModelSpatialRadial* model, const double& rho_obs, const GSkyDir& obsDir, const GEnergy& srcEng, const GTime& srcTime) const { // Set number of iterations for Romberg integration. // These values have been determined after careful testing, see // https://cta-redmine.irap.omp.eu/issues/1299 static const int iter_rho = 5; static const int iter_phi = 5; // Initialise value double irf = 0.0; // Get maximum PSF radius (radians) double delta_max = psf().delta_max(); // Set zenith angle integration range for radial model (radians) double rho_min = (rho_obs > delta_max) ? rho_obs - delta_max : 0.0; double rho_max = rho_obs + delta_max; double src_max = model->theta_max(); if (rho_max > src_max) { rho_max = src_max; } // Perform zenith angle integration if interval is valid if (rho_max > rho_min) { // Setup integration kernel. We take here the observed photon arrival // direction as the true photon arrival direction because the PSF does // not vary significantly over a small region. cta_psf_radial_kern_rho integrand(this, model, obsDir, srcEng, srcTime, rho_obs, delta_max, iter_phi); // Integrate over model's zenith angle GIntegral integral(&integrand); integral.fixed_iter(iter_rho); // Setup integration boundaries std::vector<double> bounds; bounds.push_back(rho_min); bounds.push_back(rho_max); // If the integration range includes a transition between full // containment of model within Psf and partial containment, then // add a boundary at this location double transition_point = delta_max - rho_obs; if (transition_point > rho_min && transition_point < rho_max) { bounds.push_back(transition_point); } // If we have a shell model then add an integration boundary for the // shell radius as a function discontinuity will occur at this // location const GModelSpatialRadialShell* shell = dynamic_cast<const GModelSpatialRadialShell*>(model); if (shell != NULL) { double shell_radius = shell->radius() * gammalib::deg2rad; if (shell_radius > rho_min && shell_radius < rho_max) { bounds.push_back(shell_radius); } } // Integrate kernel irf = integral.romberg(bounds, iter_rho); // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(irf) || gammalib::is_infinite(irf)) { std::cout << "*** ERROR: GCTAResponseCube::psf_radial:"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", rho_min=" << rho_min; std::cout << ", rho_max=" << rho_max << ")"; std::cout << std::endl; } #endif } // endif: integration interval is valid // Return PSF return irf; }
/***********************************************************************//** * @brief Kernel for IRF offest angle integration of the diffuse source model * * @param[in] theta Offset angle with respect to observed photon direction * (radians). * * This method provides the kernel for the IRF offset angle integration of * the diffuse source model. * * It computes * * \f[irf(\theta) = \sin \theta \times * \int_{0}^{2\pi} M(\theta, \phi) IRF(\theta, \phi) d\phi\f] * * where \f$M(\theta, \phi)\f$ is the spatial component of the diffuse * source model and \f$IRF(\theta, \phi)\f$ is the response function. * * As we assume so far an azimuthally symmetric point spread function, the * PSF computation is done outside the azimuthal integration kernel? * * Note that the integration is only performed for \f$\theta>0\f$. Otherwise * zero is returned. ***************************************************************************/ double cta_irf_diffuse_kern_theta::eval(double theta) { // Initialise result double irf = 0.0; // Continue only if offset angle is positive if (theta > 0.0) { // Get PSF value. We can do this externally to the azimuthal // integration as the PSF is so far azimuthally symmetric. Once // we introduce asymmetries, we have to move this done into the // Phi kernel method/ double psf = m_rsp->psf(theta, m_theta, m_phi, m_zenith, m_azimuth, m_srcLogEng); // Continue only if PSF is positive if (psf > 0.0) { // Precompute terms needed for the computation of the angular // distance between the true photon direction and the pointing // direction (i.e. the camera centre) double sin_theta = std::sin(theta); double cos_theta = std::cos(theta); double sin_ph = sin_theta * m_sin_eta; double cos_ph = cos_theta * m_cos_eta; // Setup kernel for azimuthal integration cta_irf_diffuse_kern_phi integrand(m_rsp, m_model, m_zenith, m_azimuth, m_srcLogEng, m_obsLogEng, m_rot, sin_theta, cos_theta, sin_ph, cos_ph); // Integrate over phi GIntegral integral(&integrand); integral.eps(1.0e-2); irf = integral.romb(0.0, twopi) * psf * sin_theta; // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (isnotanumber(irf) || isinfinite(irf)) { std::cout << "*** ERROR: cta_irf_diffuse_kern_theta"; std::cout << "(theta=" << theta << "):"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", psf=" << psf; std::cout << ", sin_theta=" << sin_theta; std::cout << ", cos_theta=" << cos_theta; std::cout << ", sin_ph=" << sin_ph; std::cout << ", cos_ph=" << cos_ph; std::cout << ")"; std::cout << std::endl; } #endif } // endif: PSF was positive } // endif: offset angle was positive // Return result return irf; }
/***********************************************************************//** * @brief Return spatially integrated background model * * @param[in] obsEng Measured event energy. * @param[in] obsTime Measured event time. * @param[in] obs Observation. * @return Spatially integrated model. * * @exception GException::invalid_argument * The specified observation is not a CTA observation. * * Spatially integrates the instrumental background model for a given * measured event energy and event time. This method also applies a deadtime * correction factor, so that the normalization of the model is a real rate * (counts/MeV/s). ***************************************************************************/ double GCTAModelIrfBackground::npred(const GEnergy& obsEng, const GTime& obsTime, const GObservation& obs) const { // Initialise result double npred = 0.0; bool has_npred = false; // Build unique identifier std::string id = obs.instrument() + "::" + 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] == obsEng) { npred = m_npred_values[i]; has_npred = true; #if defined(G_DEBUG_NPRED) std::cout << "GCTAModelIrfBackground::npred:"; 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 has been found if (!has_npred) { // Evaluate only if model is valid if (valid_model()) { // Get pointer on CTA observation const GCTAObservation* cta = dynamic_cast<const GCTAObservation*>(&obs); if (cta == NULL) { std::string msg = "Specified observation is not a CTA" " observation.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Get pointer on CTA IRF response const GCTAResponseIrf* rsp = dynamic_cast<const GCTAResponseIrf*>(cta->response()); if (rsp == NULL) { std::string msg = "Specified observation does not contain" " an IRF response.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Retrieve pointer to CTA background const GCTABackground* bgd = rsp->background(); if (bgd == NULL) { std::string msg = "Specified observation contains no background" " information.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Get CTA event list const GCTAEventList* events = dynamic_cast<const GCTAEventList*>(obs.events()); if (events == NULL) { std::string msg = "No CTA event list found in observation.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Get reference to ROI centre const GSkyDir& roi_centre = events->roi().centre().dir(); // Get ROI radius in radians double roi_radius = events->roi().radius() * gammalib::deg2rad; // Get log10 of energy in TeV double logE = obsEng.log10TeV(); // Setup integration function GCTAModelIrfBackground::npred_roi_kern_theta integrand(bgd, logE); // Setup integrator GIntegral integral(&integrand); integral.eps(g_cta_inst_background_npred_theta_eps); // Spatially integrate radial component npred = integral.romberg(0.0, roi_radius); // Store result in Npred cache #if defined(G_USE_NPRED_CACHE) m_npred_names.push_back(id); m_npred_energies.push_back(obsEng); m_npred_times.push_back(obsTime); 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::string origin = "GCTAModelIrfBackground::npred"; std::string message = " NaN/Inf encountered (npred=" + gammalib::str(npred) + ", roi_radius=" + gammalib::str(roi_radius) + ")"; gammalib::warning(origin, message); } #endif } // endif: model was valid } // endif: Npred computation required // Multiply in spectral and temporal components npred *= spectral()->eval(obsEng, obsTime); npred *= temporal()->eval(obsTime); // Apply deadtime correction npred *= obs.deadc(obsTime); // Return Npred return npred; }
/***********************************************************************//** * @brief Spatially integrate effective area for given energy * * @param[in] obs Observation. * @param[in] logE Log10 of reference energy in TeV. * @return Spatially integrated effective area for given energy. * * @exception GException::invalid_argument * Invalid observation encountered. * * Spatially integrates the effective area for a given reference energy * over the region of interest. ***************************************************************************/ double GCTAModelAeffBackground::aeff_integral(const GObservation& obs, const double& logE) const { // Initialise result double value = 0.0; // Set number of iterations for Romberg integration. static const int iter_theta = 6; static const int iter_phi = 6; // Get pointer on CTA observation const GCTAObservation* cta = dynamic_cast<const GCTAObservation*>(&obs); if (cta == NULL) { std::string msg = "Specified observation is not a CTA" " observation.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Get pointer on CTA IRF response const GCTAResponseIrf* rsp = dynamic_cast<const GCTAResponseIrf*>(cta->response()); if (rsp == NULL) { std::string msg = "Specified observation does not contain" " an IRF response.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Retrieve pointer to CTA effective area const GCTAAeff* aeff = rsp->aeff(); if (aeff == NULL) { std::string msg = "Specified observation contains no effective area" " information.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Get CTA event list const GCTAEventList* events = dynamic_cast<const GCTAEventList*>(obs.events()); if (events == NULL) { std::string msg = "No CTA event list found in observation.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } // Get ROI radius in radians double roi_radius = events->roi().radius() * gammalib::deg2rad; // Setup integration function GCTAModelAeffBackground::npred_roi_kern_theta integrand(aeff, logE, iter_phi); // Setup integration GIntegral integral(&integrand); // Set fixed number of iterations integral.fixed_iter(iter_theta); // Spatially integrate radial component value = integral.romberg(0.0, roi_radius); // Debug: Check for NaN #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(value) || gammalib::is_infinite(value)) { std::string origin = "GCTAModelAeffBackground::aeff_integral"; std::string message = " NaN/Inf encountered (value=" + gammalib::str(value) + ", roi_radius=" + gammalib::str(roi_radius) + ")"; gammalib::warning(origin, message); } #endif // Return return value; }
/***********************************************************************//** * @brief Convolve sky model with the instrument response * * @param[in] model Sky model. * @param[in] event Event. * @param[in] obs Observation. * @param[in] grad Should model gradients be computed? (default: true) * @return Event probability. * * Computes the event probability * * \f[ * P(p',E',t') = \int \int \int * S(p,E,t) \times R(p',E',t'|p,E,t) \, dp \, dE \, dt * \f] * * without taking into account any time dispersion. Energy dispersion is * correctly handled by this method. If time dispersion is indeed needed, * an instrument specific method needs to be provided. ***************************************************************************/ double GResponse::convolve(const GModelSky& model, const GEvent& event, const GObservation& obs, const bool& grad) const { // Set number of iterations for Romberg integration. static const int iter = 6; // Initialise result double prob = 0.0; // Continue only if the model has a spatial component if (model.spatial() != NULL) { // Get source time (no dispersion) GTime srcTime = event.time(); // Case A: Integration if (use_edisp()) { // Retrieve true energy boundaries GEbounds ebounds = this->ebounds(event.energy()); // Loop over all boundaries for (int i = 0; i < ebounds.size(); ++i) { // Get boundaries in MeV double emin = ebounds.emin(i).MeV(); double emax = ebounds.emax(i).MeV(); // Continue only if valid if (emax > emin) { // Setup integration function edisp_kern integrand(this, &obs, &model, &event, srcTime, grad); GIntegral integral(&integrand); // Set number of iterations integral.fixed_iter(iter); // Do Romberg integration emin = std::log(emin); emax = std::log(emax); prob += integral.romberg(emin, emax); } // endif: interval was valid } // endfor: looped over intervals } // Case B: No integration (assume no energy dispersion) else { // Get source energy (no dispersion) GEnergy srcEng = event.energy(); // Evaluate probability prob = eval_prob(model, event, srcEng, srcTime, obs, grad); } // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(prob) || gammalib::is_infinite(prob)) { std::cout << "*** ERROR: GResponse::convolve:"; std::cout << " NaN/Inf encountered"; std::cout << " (prob=" << prob; std::cout << ", event=" << event; std::cout << ", srcTime=" << srcTime; std::cout << ")" << std::endl; } #endif } // endif: spatial component valid // Return probability return prob; }
/***********************************************************************//** * @brief Return spatially integrated data model * * @param[in] obsEng Measured event energy. * @param[in] obsTime Measured event time. * @param[in] obs Observation. * @return Spatially integrated model. * * @exception GException::invalid_argument * No CTA event list found in observation. * No CTA pointing found in observation. * * Spatially integrates the data model for a given measured event energy and * event time. This method also applies a deadtime correction factor, so that * the normalization of the model is a real rate (counts/exposure time). ***************************************************************************/ double GCTAModelBackground::npred(const GEnergy& obsEng, const GTime& obsTime, const GObservation& obs) const { // Initialise result double npred = 0.0; bool has_npred = false; // Build unique identifier std::string id = obs.instrument() + "::" + 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] == obsEng) { npred = m_npred_values[i]; has_npred = true; #if defined(G_DEBUG_NPRED) std::cout << "GCTAModelBackground::npred:"; 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) { // Evaluate only if model is valid if (valid_model()) { // Get CTA event list const GCTAEventList* events = dynamic_cast<const GCTAEventList*>(obs.events()); if (events == NULL) { std::string msg = "No CTA event list found in observation.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } #if !defined(G_NPRED_AROUND_ROI) // Get CTA pointing direction GCTAPointing* pnt = dynamic_cast<GCTAPointing*>(obs.pointing()); if (pnt == NULL) { std::string msg = "No CTA pointing found in observation.\n" + obs.print(); throw GException::invalid_argument(G_NPRED, msg); } #endif // Get reference to ROI centre const GSkyDir& roi_centre = events->roi().centre().dir(); // Get ROI radius in radians double roi_radius = events->roi().radius() * gammalib::deg2rad; // Get distance from ROI centre in radians #if defined(G_NPRED_AROUND_ROI) double roi_distance = 0.0; #else double roi_distance = roi_centre.dist(pnt->dir()); #endif // Initialise rotation matrix to transform from ROI system to // celestial coordinate system GMatrix ry; GMatrix rz; ry.eulery(roi_centre.dec_deg() - 90.0); rz.eulerz(-roi_centre.ra_deg()); GMatrix rot = (ry * rz).transpose(); // Compute position angle of ROI centre with respect to model // centre (radians) #if defined(G_NPRED_AROUND_ROI) double omega0 = 0.0; #else double omega0 = pnt->dir().posang(events->roi().centre().dir()); #endif // Setup integration function GCTAModelBackground::npred_roi_kern_theta integrand(spatial(), obsEng, obsTime, rot, roi_radius, roi_distance, omega0); // Setup integrator GIntegral integral(&integrand); integral.eps(1e-3); // Setup integration boundaries #if defined(G_NPRED_AROUND_ROI) double rmin = 0.0; double rmax = roi_radius; #else double rmin = (roi_distance > roi_radius) ? roi_distance-roi_radius : 0.0; double rmax = roi_radius + roi_distance; #endif // Spatially integrate radial component npred = integral.romb(rmin, rmax); // Store result in Npred cache #if defined(G_USE_NPRED_CACHE) m_npred_names.push_back(id); m_npred_energies.push_back(obsEng); m_npred_times.push_back(obsTime); 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: GCTAModelBackground::npred:"; std::cout << " NaN/Inf encountered"; std::cout << " (npred=" << npred; std::cout << ", roi_radius=" << roi_radius; std::cout << ")" << std::endl; } #endif } // endif: model was valid } // endif: Npred computation required // Multiply in spectral and temporal components npred *= spectral()->eval(obsEng, obsTime); npred *= temporal()->eval(obsTime); // Apply deadtime correction npred *= obs.deadc(obsTime); // Return Npred return npred; }
/***********************************************************************//** * @brief Integrate Psf over radial model * * @param[in] model Radial model. * @param[in] delta_mod Angle between model centre and measured photon * direction (radians). * @param[in] obsDir Observed event direction. * @param[in] srcEng True photon energy. * @param[in] srcTime True photon arrival time. * * Integrates the product of the spatial model and the point spread * function over the true photon arrival direction using * * \f[ * \int_{\delta_{\rm min}}^{\delta_{\rm max}} * \sin \delta \times {\rm Psf}(\delta) \times * \int_{\phi_{\rm min}}^{\phi_{\rm max}} * S_{\rm p}(\delta, \phi | E, t) d\phi d\delta * \f] * * where * \f$S_{\rm p}(\delta, \phi | E, t)\f$ is the radial spatial model, * \f${\rm Psf}(\delta)\f$ is the point spread function, * \f$\delta\f$ is angular distance between the true and the measured * photon direction, and * \f$\phi\f$ is the position angle around the observed photon direction * measured counterclockwise from the connecting line between the model * centre and the observed photon arrival direction. ***************************************************************************/ double GCTAResponseCube::psf_radial(const GModelSpatialRadial* model, const double& delta_mod, const GSkyDir& obsDir, const GEnergy& srcEng, const GTime& srcTime) const { // Set number of iterations for Romberg integration. // These values have been determined after careful testing, see // https://cta-redmine.irap.omp.eu/issues/1291 static const int iter_delta = 5; static const int iter_phi = 6; // Initialise value double value = 0.0; // Get maximum Psf radius (radians) double psf_max = psf().delta_max(); // Get maximum model radius (radians) double theta_max = model->theta_max(); // Set offset angle integration range (radians) double delta_min = (delta_mod > theta_max) ? delta_mod - theta_max : 0.0; double delta_max = delta_mod + theta_max; if (delta_max > psf_max) { delta_max = psf_max; } // Setup integration kernel. We take here the observed photon arrival // direction as the true photon arrival direction because the PSF does // not vary significantly over a small region. cta_psf_radial_kern_delta integrand(this, model, obsDir, srcEng, srcTime, delta_mod, theta_max, iter_phi); // Integrate over model's zenith angle GIntegral integral(&integrand); integral.fixed_iter(iter_delta); // Setup integration boundaries std::vector<double> bounds; bounds.push_back(delta_min); bounds.push_back(delta_max); // If the integration range includes a transition between full // containment of Psf within model and partial containment, then // add a boundary at this location double transition_point = theta_max - delta_mod; if (transition_point > delta_min && transition_point < delta_max) { bounds.push_back(transition_point); } // Integrate kernel value = integral.romberg(bounds, iter_delta); // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(value) || gammalib::is_infinite(value)) { std::cout << "*** ERROR: GCTAResponseCube::psf_radial:"; std::cout << " NaN/Inf encountered"; std::cout << " (value=" << value; std::cout << ", delta_min=" << delta_min; std::cout << ", delta_max=" << delta_max << ")"; std::cout << std::endl; } #endif // Return integral return value; }
/***********************************************************************//** * @brief Integrate Psf over elliptical model * * @param[in] model Elliptical model. * @param[in] rho_obs Angle between model centre and measured photon direction * (radians). * @param[in] posangle_obs Position angle of measured photon direction with * respect to model centre (radians). * @param[in] obsDir Observed event direction. * @param[in] srcEng True photon energy. * @param[in] srcTime True photon arrival time. * * Integrates the product of the spatial model and the point spread * function over the true photon arrival direction using * * \f[ * \int_{\rho_{\rm min}}^{\rho_{\rm max}} * \sin \rho \times * \int_{\omega} * S_{\rm p}(\rho, \omega | E, t) \times * {\rm Psf}(\rho, \omega) d\omega d\rho * \f] * * where * \f$S_{\rm p}(\rho, \omega | E, t)\f$ is the elliptical spatial model, * \f${\rm Psf}(\rho, \omega)\f$ is the point spread function, * \f$\rho\f$ is the radial distance from the model centre, and * \f$\omega\f$ is the position angle around the model centre measured * counterclockwise from the connecting line between the model centre and * the observed photon arrival direction. ***************************************************************************/ double GCTAResponseCube::psf_elliptical(const GModelSpatialElliptical* model, const double& rho_obs, const double& posangle_obs, const GSkyDir& obsDir, const GEnergy& srcEng, const GTime& srcTime) const { // Set number of iterations for Romberg integration. // These values have been determined after careful testing, see // https://cta-redmine.irap.omp.eu/issues/1299 static const int iter_rho = 5; static const int iter_phi = 5; // Initialise value double irf = 0.0; // Get maximum PSF radius (radians) double delta_max = psf().delta_max(); // Get the ellipse boundary (radians). Note that these are NOT the // parameters of the ellipse but of a boundary ellipse that is used // for computing the relevant omega angle intervals for a given angle // rho. The boundary ellipse takes care of the possibility that the // semiminor axis is larger than the semimajor axis double semimajor; // Will be the larger axis double semiminor; // Will be the smaller axis double posangle; // Will be the corrected position angle double aspect_ratio; // Ratio between smaller/larger axis of model if (model->semimajor() >= model->semiminor()) { aspect_ratio = (model->semimajor() > 0.0) ? model->semiminor() / model->semimajor() : 0.0; posangle = model->posangle() * gammalib::deg2rad; } else { aspect_ratio = (model->semiminor() > 0.0) ? model->semimajor() / model->semiminor() : 0.0; posangle = model->posangle() * gammalib::deg2rad + gammalib::pihalf; } semimajor = model->theta_max(); semiminor = semimajor * aspect_ratio; // Set zenith angle integration range for elliptical model double rho_min = (rho_obs > delta_max) ? rho_obs - delta_max : 0.0; double rho_max = rho_obs + delta_max; if (rho_max > semimajor) { rho_max = semimajor; } // Perform zenith angle integration if interval is valid if (rho_max > rho_min) { // Setup integration kernel. We take here the observed photon arrival // direction as the true photon arrival direction because the PSF does // not vary significantly over a small region. cta_psf_elliptical_kern_rho integrand(this, model, semimajor, semiminor, posangle, obsDir, srcEng, srcTime, rho_obs, posangle_obs, delta_max, iter_phi); // Integrate over model's zenith angle GIntegral integral(&integrand); integral.fixed_iter(iter_rho); // Setup integration boundaries std::vector<double> bounds; bounds.push_back(rho_min); bounds.push_back(rho_max); // Kluge: add this transition point as this allows to fit the test // case without any stalls. Not clear why this is the case, maybe // simply because the rho integral gets cut down into one more // sub-interval which may increase precision and smoothed the // likelihood contour double transition_point = delta_max - rho_obs; if (transition_point > rho_min && transition_point < rho_max) { bounds.push_back(transition_point); } // If the integration range includes the semiminor boundary, then // add an integration boundary at that location if (semiminor > rho_min && semiminor < rho_max) { bounds.push_back(semiminor); } // Integrate kernel irf = integral.romberg(bounds, iter_rho); // Compile option: Check for NaN/Inf #if defined(G_NAN_CHECK) if (gammalib::is_notanumber(irf) || gammalib::is_infinite(irf)) { std::cout << "*** ERROR: GCTAResponseCube::psf_elliptical:"; std::cout << " NaN/Inf encountered"; std::cout << " (irf=" << irf; std::cout << ", rho_min=" << rho_min; std::cout << ", rho_max=" << rho_max << ")"; std::cout << std::endl; } #endif } // endif: integration interval is valid // Return PSF return irf; }