/***************************************************************************
* GObservation.cpp - Abstract observation base class *
* ----------------------------------------------------------------------- *
* copyright (C) 2008-2017 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 GObservation.cpp
* @brief Abstract observation base class implementation
* @author Juergen Knoedlseder
*/
/* __ Includes ___________________________________________________________ */
#ifdef HAVE_CONFIG_H
#include
#endif
#include "GTools.hpp"
#include "GException.hpp"
#include "GIntegral.hpp"
#include "GDerivative.hpp"
#include "GVector.hpp"
#include "GMatrixSparse.hpp"
#include "GObservation.hpp"
#include "GResponse.hpp"
#include "GEventCube.hpp"
#include "GEventList.hpp"
#include "GEventBin.hpp"
#include "GModel.hpp"
#include "GModels.hpp"
#include "GModelPar.hpp"
#include "GModelSky.hpp"
#include "GModelData.hpp"
/* __ Method name definitions ____________________________________________ */
#define G_LIKELIHOOD "GObservation::likelihood(GModels&, GVector*,"\
" GMatrixSparse*, double*)"
#define G_MODEL "GObservation::model(GModels&, GPointing&,"\
" GInstDir&, GEnergy&, GTime&, GVector*)"
#define G_EVENTS "GObservation::events()"
#define G_NPRED "GObservation::npred(GModel&)"
#define G_NPRED_SPEC "GObservation::npred_spec(GModel&, GTime&)"
/* __ Constants __________________________________________________________ */
const double minmod = 1.0e-100; //!< Minimum model value
const double minerr = 1.0e-100; //!< Minimum statistical error
/* __ Macros _____________________________________________________________ */
/* __ Coding definitions _________________________________________________ */
#define G_LN_ENERGY_INT //!< ln(E) variable substitution for integration
/* __ Debug definitions __________________________________________________ */
//#define G_OPT_DEBUG
/*==========================================================================
= =
= Constructors/destructors =
= =
==========================================================================*/
/***********************************************************************//**
* @brief Void constructor
***************************************************************************/
GObservation::GObservation(void)
{
// Initialise members
init_members();
// Return
return;
}
/***********************************************************************//**
* @brief Copy constructor
*
* @param[in] obs Observation.
*
* Instantiate the class by copying from an existing observation.
***************************************************************************/
GObservation::GObservation(const GObservation& obs)
{
// Initialise members
init_members();
// Copy members
copy_members(obs);
// Return
return;
}
/***********************************************************************//**
* @brief Destructor
***************************************************************************/
GObservation::~GObservation(void)
{
// Free members
free_members();
// Return
return;
}
/*==========================================================================
= =
= Operators =
= =
==========================================================================*/
/***********************************************************************//**
* @brief Assignment operator
*
* @param[in] obs Observation.
* @return Observation.
*
* Assign observation.
***************************************************************************/
GObservation& GObservation::operator=(const GObservation& obs)
{
// Execute only if object is not identical
if (this != &obs) {
// Free members
free_members();
// Initialise members
init_members();
// Copy members
copy_members(obs);
} // endif: object was not identical
// Return this object
return *this;
}
/*==========================================================================
= =
= Public methods =
= =
==========================================================================*/
/***********************************************************************//**
* @brief Compute likelihood function
*
* @param[in] models Models.
* @param[in,out] gradient Pointer to gradients.
* @param[in,out] curvature Pointer to curvature matrix.
* @param[in,out] npred Pointer to Npred value.
* @return Likelihood.
*
* Computes the likelihood for a specified set of models. The method also
* returns the gradients, the curvature matrix, and the number of events
* that are predicted by all models.
***************************************************************************/
double GObservation::likelihood(const GModels& models,
GVector* gradient,
GMatrixSparse* curvature,
double* npred) const
{
// Initialise likelihood value
double value = 0.0;
// Extract statistic for this observation
std::string statistic = gammalib::toupper(this->statistic());
// Unbinned analysis
if (dynamic_cast(events()) != NULL) {
// Poisson statistic
if ((statistic == "POISSON") || (statistic == "CSTAT")) {
// Update the log-likelihood
value = likelihood_poisson_unbinned(models,
gradient,
curvature,
npred);
} // endif: Poisson statistic
// ... otherwise throw an exception
else {
std::string msg = "Invalid statistic \""+statistic+"\". Unbinned "
"optimization requires \"POISSON\" or \"CSTAT\" "
"statistic.";
throw GException::invalid_value(G_LIKELIHOOD, msg);
}
} // endif: unbinned analysis
// ... or binned analysis
else {
// Poisson statistic
if ((statistic == "POISSON") || (statistic == "CSTAT")) {
value = likelihood_poisson_binned(models,
gradient,
curvature,
npred);
}
// ... or Gaussian statistic
else if ((statistic == "GAUSSIAN") || (statistic == "CHI2")) {
value = likelihood_gaussian_binned(models,
gradient,
curvature,
npred);
}
// ... or unsupported
else {
std::string msg = "Invalid statistic \""+statistic+"\". Binned "
"optimization requires \"POISSON\", \"CSTAT\", "
"\"GAUSSIAN\" or \"CHI2\" statistic.";
throw GException::invalid_value(G_LIKELIHOOD, msg);
}
} // endelse: binned analysis
// Return likelihood
return value;
}
/***********************************************************************//**
* @brief Return model value and (optionally) gradient
*
* @param[in] models Model descriptor.
* @param[in] event Observed event.
* @param[out] gradient Pointer to gradient vector (optional).
*
* @exception GException::invalid_value
* Dimension of gradient vector mismatches number of parameters.
*
* Implements generic model and gradient evaluation for an observation. The
* model gives the probability for an event to occur with a given instrument
* direction, at a given energy and at a given time. The gradient is the
* parameter derivative of this probability. If NULL is passed for the
* gradient vector, then gradients will not be computed.
*
* The method will only operate on models for which the list of instruments
* and observation identifiers matches those of the observation. Models that
* do not match will be skipped.
***************************************************************************/
double GObservation::model(const GModels& models,
const GEvent& event,
GVector* gradient) const
{
// Initialise method variables
double model = 0.0; // Reset model value
int igrad = 0; // Reset gradient counter
int grad_size = 0; // Reset gradient size
// If gradient is available then reset gradient vector elements to 0
// and determine vector size
if (gradient != NULL) {
(*gradient) = 0.0;
grad_size = gradient->size();
}
// Loop over models
for (int i = 0; i < models.size(); ++i) {
// Get model pointer. Continue only if pointer is valid
const GModel* mptr = models[i];
if (mptr != NULL) {
// Continue only if model applies to specific instrument and
// observation identifier
if (mptr->is_valid(instrument(), id())) {
// Compute value and add to model. If energy dispersion
// is used, don't compute model gradients as we cannot
// use them. This is somehow a kluge, but makes the
// code faster
model += mptr->eval(event, *this, !response()->use_edisp());
// Optionally determine model gradients. If the model has a
// gradient then use it, unless we have energy dispersion.
// For energy dispersion, no gradients are available as we
// have not implemented code that integrates the gradients
// over the energy dispersion. Here it's simpler to just use
// numerical gradients.
if (gradient != NULL) {
for (int ipar = 0; ipar < mptr->size(); ++ipar) {
// Get reference to model parameter
const GModelPar& par = (*mptr)[ipar];
// Make sure that we have a slot for the gradient
#if defined(G_RANGE_CHECK)
if (igrad >= grad_size) {
std::string msg = "Vector has not enough elements "
"to store the model parameter "
"gradients. "+
gammalib::str(models.npars())+
" elements requested while vector "
"only contains "+
gammalib::str(gradient->size())+
" elements.";
throw GException::invalid_value(G_MODEL, msg);
}
#endif
if (par.is_free()) {
if (par.has_grad() && !response()->use_edisp()) {
(*gradient)[igrad+ipar] = par.factor_gradient();
}
else {
(*gradient)[igrad+ipar] = model_grad(*mptr, par, event);
}
}
else {
(*gradient)[igrad+ipar] = 0.0;
}
}
}
} // endif: model component was valid for instrument
// Increment parameter counter for gradients
igrad += mptr->size();
} // endif: model was valid
} // endfor: Looped over models
// Return
return model;
}
/***********************************************************************//**
* @brief Return total number of observed events
*
* @returns Total number of observed events.
*
* Returns the total number of observed events. If the observation does not
* contain any events the method returns zero.
***************************************************************************/
int GObservation::nobserved(void) const
{
// Initialise number of observed events
int nobserved = 0;
// Extract number of observed events
if (m_events != NULL) {
nobserved = m_events->number();
}
// Return number of observed events
return nobserved;
}
/***********************************************************************//**
* @brief Return total number (and optionally gradient) of predicted counts
* for all models
*
* @param[in] models Models.
* @param[out] gradient Model parameter gradients (optional).
*
* @exception GException::gradient_par_mismatch
* Dimension of gradient vector mismatches number of parameters.
*
* Returns the total number of predicted counts within the analysis region.
* If NULL is passed for the gradient vector then gradients will not be
* computed.
*
* The method will only operate on models for which the list of instruments
* and observation identifiers matches those of the observation. Models that
* do not match will be skipped.
***************************************************************************/
double GObservation::npred(const GModels& models, GVector* gradient) const
{
// Verify that gradient vector and models have the same dimension
#if defined(G_RANGE_CHECK)
if (gradient != NULL) {
if (models.npars() != gradient->size()) {
throw GException::gradient_par_mismatch(G_MODEL,
gradient->size(),
models.npars());
}
}
#endif
// Initialise
double npred = 0.0; // Reset predicted number of counts
int igrad = 0; // Reset gradient counter
// If gradient is available then reset gradient vector elements to 0
if (gradient != NULL) {
(*gradient) = 0.0;
}
// Loop over models
for (int i = 0; i < models.size(); ++i) {
// Get model pointer. Continue only if pointer is valid
const GModel* mptr = models[i];
if (mptr != NULL) {
// Continue only if model applies to specific instrument and
// observation identifier
if (mptr->is_valid(instrument(), id())) {
// Determine Npred for model
npred += this->npred(*mptr);
// Optionally determine Npred gradients
if (gradient != NULL) {
for (int k = 0; k < mptr->size(); ++k) {
const GModelPar& par = (*mptr)[k];
(*gradient)[igrad+k] = npred_grad(*mptr, par);
}
}
} // endif: model component was valid for instrument
// Increment parameter counter for gradient
igrad += mptr->size();
} // endif: model was valid
} // endfor: Looped over models
// Return prediction
return npred;
}
/***********************************************************************//**
* @brief Return total number of predicted counts for one model
*
* @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}
* P(p',E',t') \, dp' \, dE' \, dt'
* \f]
*
* of 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]
*
* where
* \f$S(p,E,t)\f$ is the source model,
* \f$R(p',E',t'|p,E,t)\f$ is the instrument response function,
* \f$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$p\f$ is the true photon arrival direction,
* \f$E\f$ is the true photon energy, and
* \f$t\f$ is the true photon arrival time.
*
* This method performs the time integration over the Good Time Intervals
* \f${\rm GTI}\f$. 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(const GModel& model) const
{
// Initialise result
double npred = 0.0;
// Continue only if model applies to specific instrument and
// observation identifier
if (model.is_valid(instrument(), id())) {
// Case A: If the model is constant then integrate analytically
if (model.is_constant()) {
// Evaluate model at first start time and multiply by ontime
double ontime = events()->gti().ontime();
// Integrate only if ontime is positive
if (ontime > 0.0) {
// Integration is a simple multiplication by the time
npred = npred_spec(model, events()->gti().tstart()) * ontime;
}
} // endif: model was constant
// ... otherwise integrate temporally
else {
// 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,
events()->gti().tstart(i),
events()->gti().tstop(i));
}
// Setup integration function
GObservation::npred_kern integrand(this, &model);
GIntegral integral(&integrand);
// Do Romberg integration
npred += integral.romberg(tstart, tstop);
} // endfor: looped over GTIs
} // endelse: integrated temporally
} // endif: model was valid
// Return Npred
return npred;
}
/***********************************************************************//**
* @brief Returns parameter gradient of model for a given event
*
* @param[in] model Model.
* @param[in] par Model parameter.
* @param[in] event Event.
*
* This method uses a robust but simple difference method to estimate
* parameter gradients that have not been provided by the model. We use here
* a simple method as this method is likely used for spatial model parameters,
* and the spatial model may eventually be noisy due to numerical integration
* limits.
*
* The step size for the simple method has been fixed to 0.0002, which
* corresponds to about 1 arcsec for parameters that are given in degrees.
* The reasoning behind this value is that parameters that use numerical
* gradients are typically angles, such as for example the position, and
* we want to achieve arcsec precision with this method.
***************************************************************************/
double GObservation::model_grad(const GModel& model,
const GModelPar& par,
const GEvent& event) const
{
// Initialise gradient
double grad = 0.0;
// Compute gradient only if parameter is free
if (par.is_free()) {
// Get non-const model pointer
GModelPar* ptr = const_cast(&par);
// Save current model parameter
GModelPar current = par;
// Get actual parameter value
double x = par.factor_value();
// Set fixed step size for computation of derivative.
// By default, the step size is fixed to 0.0002.
const double step_size = 0.0002; // ~1 arcsec
double h = step_size;
// Re-adjust the step-size h in case that the initial step size is
// larger than the allowed parameter range
if (par.has_min() && par.has_max()) {
double par_h = par.factor_max() - par.factor_min();
if (par_h < h) {
h = par_h;
}
}
// Continue only if step size is positive
if (h > 0.0) {
// Remove any boundaries to avoid limitations
//ptr->remove_range(); // Not needed in principle
// Setup derivative function
GObservation::model_func function(this, &model, ptr, &event);
GDerivative derivative(&function);
// If we are too close to the minimum boundary use a right sided
// difference ...
if (par.has_min() && ((x-par.factor_min()) < h)) {
grad = derivative.right_difference(x, h);
}
// ... otherwise if we are too close to the maximum boundary use
// a left sided difference ...
else if (par.has_max() && ((par.factor_max()-x) < h)) {
grad = derivative.left_difference(x, h);
}
// ... otherwise use a symmetric difference
else {
grad = derivative.difference(x, h);
}
} // endif: step size was positive
// Restore current model parameter
*ptr = current;
} // endif: model parameter was free
// Return gradient
return grad;
}
/***********************************************************************//**
* @brief Returns parameter gradient of Npred
*
* @param[in] model Gamma-ray source model.
* @param[in] par Model parameter.
*
* Computes
*
* \f[
* \frac{{\rm d} N_{\rm pred}}{{\rm d} a_i}
* \f]
*
* where
*
* \f[
* N_{\rm pred} = \int_{\rm GTI} \int_{E_{\rm bounds}} \int_{\rm ROI}
* P(p',E',t') \, dp' \, dE' \, dt'
* \f]
*
* is the integral of 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]
*
* and
* \f$a_i\f$ is the model parameter \f$i\f$.
* Furthermore
* \f$S(p,E,t)\f$ is the source model,
* \f$R(p',E',t'|p,E,t)\f$ is the instrument response function,
* \f$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$p\f$ is the true photon arrival direction,
* \f$E\f$ is the true photon energy, and
* \f$t\f$ is the true photon arrival time.
*
* This method uses a robust but simple difference method to estimate
* parameter gradients that have not been provided by the model. We use here
* a simple method as this method is likely used for spatial model parameters,
* and the spatial model may eventually be noisy due to numerical integration
* limits.
*
* The step size for the simple method has been fixed to 0.0002, which
* corresponds to about 1 arcsec for parameters that are given in degrees.
* The reasoning behind this value is that parameters that use numerical
* gradients are typically angles, such as for example the position, and
* we want to achieve arcsec precision with this method.
***************************************************************************/
double GObservation::npred_grad(const GModel& model, const GModelPar& par) const
{
// Initialise result
double grad = 0.0;
// Compute gradient only if parameter is free
if (par.is_free()) {
// Get non-const model pointer
GModelPar* ptr = const_cast(&par);
// Save current model parameter
GModelPar current = par;
// Get actual parameter value
double x = par.factor_value();
// Set fixed step size for computation of derivative.
// By default, the step size is fixed to 0.0002.
const double step_size = 0.0002; // ~1 arcsec
double h = step_size;
// Re-adjust the step-size h in case that the initial step size is
// larger than the allowed parameter range
if (par.has_min() && par.has_max()) {
double par_h = par.factor_max() - par.factor_min();
if (par_h < h) {
h = par_h;
}
}
// Continue only if step size is positive
if (h > 0.0) {
// Remove any boundaries to avoid limitations
//ptr->remove_range(); // Not needed in principle
// Setup derivative function
GObservation::npred_func function(this, &model, ptr);
GDerivative derivative(&function);
// If we are too close to the minimum boundary use a right sided
// difference ...
if (par.has_min() && ((x-par.factor_min()) < h)) {
grad = derivative.right_difference(x, h);
}
// ... otherwise if we are too close to the maximum boundary use
// a left sided difference ...
else if (par.has_max() && ((par.factor_max()-x) < h)) {
grad = derivative.left_difference(x, h);
}
// ... otherwise use a symmetric difference
else {
grad = derivative.difference(x, h);
}
} // endif: step size was positive
// Restore current model parameter
*ptr = current;
} // endif: model parameter was free
// Return result
return grad;
}
/***********************************************************************//**
* @brief Set event container
*
* @param[in] events Event container.
*
* Set the event container for this observation by cloning the container
* specified in the argument.
***************************************************************************/
void GObservation::events(const GEvents& events)
{
// Remove an existing event container
if (m_events != NULL) delete m_events;
// Clone events
m_events = events.clone();
// Return
return;
}
/***********************************************************************//**
* @brief Return events
*
* @exception GException::no_events
* No events allocated for observation.
*
* Returns pointer to events.
***************************************************************************/
GEvents* GObservation::events(void)
{
// Throw an exception if the event container is not valid
if (m_events == NULL) {
std::string msg = "No events allocated for observation.";
throw GException::invalid_value(G_EVENTS, msg);
}
// Return pointer to events
return (m_events);
}
/***********************************************************************//**
* @brief Return events (const version)
*
* @exception GException::no_events
* No events allocated for observation.
*
* Returns const pointer to events.
***************************************************************************/
const GEvents* GObservation::events(void) const
{
// Throw an exception if the event container is not valid
if (m_events == NULL) {
std::string msg = "No events allocated for observation.";
throw GException::invalid_value(G_EVENTS, msg);
}
// Return pointer to events
return (m_events);
}
/*==========================================================================
= =
= Private methods =
= =
==========================================================================*/
/***********************************************************************//**
* @brief Initialise class members
***************************************************************************/
void GObservation::init_members(void)
{
// Initialise members
m_name.clear();
m_id.clear();
m_statistic = "cstat";
m_events = NULL;
// Return
return;
}
/***********************************************************************//**
* @brief Copy class members
*
* @param[in] obs Observation.
*
* Copy members from an observation.
***************************************************************************/
void GObservation::copy_members(const GObservation& obs)
{
// Copy members
m_name = obs.m_name;
m_id = obs.m_id;
m_statistic = obs.m_statistic;
// Clone members
m_events = (obs.m_events != NULL) ? obs.m_events->clone() : NULL;
// Return
return;
}
/***********************************************************************//**
* @brief Delete class members
***************************************************************************/
void GObservation::free_members(void)
{
// Free members
if (m_events != NULL) delete m_events;
// Signal free pointers
m_events = NULL;
// Return
return;
}
/*==========================================================================
= =
= Likelihood methods =
= =
==========================================================================*/
/***********************************************************************//**
* @brief Evaluate log-likelihood function for Poisson statistic and
* unbinned analysis (version with working arrays)
*
* @param[in] models Models.
* @param[in,out] gradient Gradient.
* @param[in,out] curvature Curvature matrix.
* @param[in,out] npred Number of predicted events.
* @return Likelihood value.
*
* This method evaluates the -(log-likelihood) function for parameter
* optimisation using unbinned analysis and Poisson statistic.
* The -(log-likelihood) function is given by
*
* \f[
* L = N_{\rm pred} - \sum_i \log e_i
* \f]
*
* where
* \f$N_{\rm pred}\f$ is the number of events predicted by the model, and
* the sum is taken over all events. This method also computes the
* parameter gradients
* \f$\delta L/dp\f$
* and the curvature matrix
* \f$\delta^2 L/dp_1 dp_2\f$.
***************************************************************************/
double GObservation::likelihood_poisson_unbinned(const GModels& models,
GVector* gradient,
GMatrixSparse* curvature,
double* npred) const
{
// Initialise likelihood value
double value = 0.0;
// Get number of parameters
int npars = gradient->size();
// Allocate some working arrays
int* inx = new int[npars];
double* values = new double[npars];
GVector wrk_grad(npars);
// Determine Npred value and gradient for this observation
double npred_value = this->npred(models, &wrk_grad);
// Update likelihood, Npred and gradient
value += npred_value;
*npred += npred_value;
*gradient += wrk_grad;
// Iterate over all events
for (int i = 0; i < events()->size(); ++i) {
// Get event pointer
const GEvent* event = (*events())[i];
// Get model and derivative
double model = this->model(models, *event, &wrk_grad);
// Skip bin if model is too small (avoids -Inf or NaN gradients)
if (model <= minmod) {
continue;
}
// Create index array of non-zero derivatives and initialise working
// array
int ndev = 0;
for (int i = 0; i < npars; ++i) {
values[i] = 0.0;
if (wrk_grad[i] != 0.0 && !gammalib::is_infinite(wrk_grad[i])) {
inx[ndev] = i;
ndev++;
}
}
// Update Poissonian statistic (excluding factorial term for faster
// computation)
value -= log(model);
// Skip bin now if there are no non-zero derivatives
if (ndev < 1) {
continue;
}
// Update gradient vector and curvature matrix.
double fb = 1.0 / model;
double fa = fb / model;
for (int jdev = 0; jdev < ndev; ++jdev) {
// Initialise computation
register int jpar = inx[jdev];
double g = wrk_grad[jpar];
double fa_i = fa * g;
// Update gradient.
(*gradient)[jpar] -= fb * g;
// Loop over rows
register int* ipar = inx;
for (register int idev = 0; idev < ndev; ++idev, ++ipar) {
values[idev] = fa_i * wrk_grad[*ipar];
}
// Add column to matrix
curvature->add_to_column(jpar, values, inx, ndev);
} // endfor: looped over columns
} // endfor: iterated over all events
// Free temporary memory
if (values != NULL) delete [] values;
if (inx != NULL) delete [] inx;
// Return
return value;
}
/***********************************************************************//**
* @brief Evaluate log-likelihood function for Poisson statistic and
* binned analysis (version with working arrays)
*
* @param[in] models Models.
* @param[in,out] gradient Gradient.
* @param[in,out] curvature Curvature matrix.
* @param[in,out] npred Number of predicted events.
* @return Likelihood value.
*
* This method evaluates the -(log-likelihood) function for parameter
* optimisation using binned analysis and Poisson statistic.
* The -(log-likelihood) function is given by
*
* \f[
* L=-\sum_i n_i \log e_i - e_i
* \f]
*
* where the sum is taken over all data space bins, \f$n_i\f$ is the
* observed number of counts and \f$e_i\f$ is the model.
* This method also computes the parameter gradients
* \f$\delta L/dp\f$
* and the curvature matrix
* \f$\delta^2 L/dp_1 dp_2\f$
* and also updates the total number of predicted events m_npred.
***************************************************************************/
double GObservation::likelihood_poisson_binned(const GModels& models,
GVector* gradient,
GMatrixSparse* curvature,
double* npred) const
{
// Initialise likelihood value
double value = 0.0;
// Initialise statistic
#if defined(G_OPT_DEBUG)
int n_bins = 0;
int n_used = 0;
int n_small_model = 0;
int n_zero_data = 0;
int n_exclude_data = 0;
double sum_data = 0.0;
double sum_model = 0.0;
double init_value = value;
#endif
// Get number of parameters
int npars = gradient->size();
// Allocate some working arrays
int* inx = new int[npars];
double* values = new double[npars];
GVector wrk_grad(npars);
// Iterate over all bins
for (int i = 0; i < events()->size(); ++i) {
// Update number of bins
#if defined(G_OPT_DEBUG)
n_bins++;
#endif
// Get event pointer
const GEventBin* bin =
(*(static_cast(const_cast(events()))))[i];
// Get number of counts in bin
double data = bin->counts();
// Skip bin if data is negative (filtering flag)
if (data < 0) {
#if defined(G_OPT_DEBUG)
n_exclude_data++;
#endif
continue;
}
// Get model and derivative
double model = this->model(models, *bin, &wrk_grad);
// Multiply model by bin size
model *= bin->size();
// Skip bin if model is too small (avoids -Inf or NaN gradients)
if (model <= minmod) {
#if defined(G_OPT_DEBUG)
n_small_model++;
#endif
continue;
}
// Update statistic
#if defined(G_OPT_DEBUG)
n_used++;
sum_data += data;
sum_model += model;
#endif
// Update Npred
*npred += model;
// Multiply gradient by bin size
wrk_grad *= bin->size();
// Create index array of non-zero derivatives and initialise working
// array
int ndev = 0;
for (int i = 0; i < npars; ++i) {
values[i] = 0.0;
if (wrk_grad[i] != 0.0 && !gammalib::is_infinite(wrk_grad[i])) {
inx[ndev] = i;
ndev++;
}
}
// Update gradient vector and curvature matrix. To avoid
// unneccessary computations we distinguish the case where
// data>0 and data=0. The second case requires much less
// computation since it does not contribute to the curvature
// matrix ...
if (data > 0.0) {
// Update Poissonian statistic (excluding factorial term for
// faster computation)
value -= data * log(model) - model;
// Skip bin now if there are no non-zero derivatives
if (ndev < 1) {
continue;
}
// Pre computation
double fb = data / model;
double fc = (1.0 - fb);
double fa = fb / model;
// Loop over columns
for (int jdev = 0; jdev < ndev; ++jdev) {
// Initialise computation
register int jpar = inx[jdev];
double g = wrk_grad[jpar];
double fa_i = fa * g;
// Update gradient
(*gradient)[jpar] += fc * g;
// Loop over rows
register int* ipar = inx;
for (register int idev = 0; idev < ndev; ++idev, ++ipar) {
values[idev] = fa_i * wrk_grad[*ipar];
}
// Add column to matrix
curvature->add_to_column(jpar, values, inx, ndev);
} // endfor: looped over columns
} // endif: data was > 0
// ... handle now data=0
else {
// Update statistic
#if defined(G_OPT_DEBUG)
n_zero_data++;
#endif
// Update Poissonian statistic (excluding factorial term for
// faster computation)
value += model;
// Skip bin now if there are no non-zero derivatives
if (ndev < 1) {
continue;
}
// Update gradient
register int* ipar = inx;
for (register int idev = 0; idev < ndev; ++idev, ++ipar) {
(*gradient)[*ipar] += wrk_grad[*ipar];
}
} // endif: data was 0
} // endfor: iterated over all events
// Free temporary memory
if (values != NULL) delete [] values;
if (inx != NULL) delete [] inx;
// Dump statistic
#if defined(G_OPT_DEBUG)
std::cout << "Number of bins: " << n_bins << std::endl;
std::cout << "Number of bins used for computation: " << n_used << std::endl;
std::cout << "Number of bins excluded from data: " << n_exclude_data << std::endl;
std::cout << "Number of bins excluded due to small model: " << n_small_model << std::endl;
std::cout << "Number of bins with zero data: " << n_zero_data << std::endl;
std::cout << "Sum of data: " << sum_data << std::endl;
std::cout << "Sum of model: " << sum_model << std::endl;
std::cout << "Initial statistic: " << init_value << std::endl;
std::cout << "Statistic: " << m_value-init_value << std::endl;
#endif
// Return
return value;
}
/***********************************************************************//**
* @brief Evaluate log-likelihood function for Gaussian statistic and
* binned analysis (version with working arrays)
*
* @param[in] models Models.
* @param[in,out] gradient Gradient.
* @param[in,out] curvature Curvature matrix.
* @param[in,out] npred Number of predicted events.
* @return Likelihood value.
*
* This method evaluates the -(log-likelihood) function for parameter
* optimisation using binned analysis and Poisson statistic.
* The -(log-likelihood) function is given by
*
* \f[
* L = 1/2 \sum_i (n_i - e_i)^2 \sigma_i^{-2}
* \f]
*
* where the sum is taken over all data space bins, \f$n_i\f$ is the
* observed number of counts, \f$e_i\f$ is the model and \f$\sigma_i\f$
* is the statistical uncertainty.
* This method also computes the parameter gradients
* \f$\delta L/dp\f$
* and the curvature matrix
* \f$\delta^2 L/dp_1 dp_2\f$
* and also updates the total number of predicted events m_npred.
***************************************************************************/
double GObservation::likelihood_gaussian_binned(const GModels& models,
GVector* gradient,
GMatrixSparse* curvature,
double* npred) const
{
// Initialise likelihood value
double value = 0.0;
// Get number of parameters
int npars = gradient->size();
// Allocate some working arrays
int* inx = new int[npars];
double* values = new double[npars];
GVector wrk_grad(npars);
// Iterate over all bins
for (int i = 0; i < events()->size(); ++i) {
// Get event pointer
const GEventBin* bin =
(*(static_cast(const_cast(events()))))[i];
// Get number of counts in bin
double data = bin->counts();
// Skip bin if data is negative (filtering flag)
if (data < 0) {
continue;
}
// Get statistical uncertainty
double sigma = bin->error();
// Skip bin if statistical uncertainty is too small
if (sigma <= minerr) {
continue;
}
// Get model and derivative
double model = this->model(models, *bin, &wrk_grad);
// Multiply model by bin size
model *= bin->size();
// Skip bin if model is too small (avoids -Inf or NaN gradients)
if (model <= minmod) {
continue;
}
// Update Npred
*npred += model;
// Multiply gradient by bin size
wrk_grad *= bin->size();
// Create index array of non-zero derivatives and initialise working
// array
int ndev = 0;
for (int i = 0; i < npars; ++i) {
values[i] = 0.0;
if (wrk_grad[i] != 0.0 && !gammalib::is_infinite(wrk_grad[i])) {
inx[ndev] = i;
ndev++;
}
}
// Set weight
double weight = 1.0 / (sigma * sigma);
// Update Gaussian statistic
double fa = data - model;
value += 0.5 * (fa * fa * weight);
// Skip bin now if there are no non-zero derivatives
if (ndev < 1) {
continue;
}
// Loop over columns
for (int jdev = 0; jdev < ndev; ++jdev) {
// Initialise computation
register int jpar = inx[jdev];
double fa_i = wrk_grad[jpar] * weight;
// Update gradient
(*gradient)[jpar] -= fa * fa_i;
// Loop over rows
register int* ipar = inx;
for (register int idev = 0; idev < ndev; ++idev, ++ipar) {
values[idev] = fa_i * wrk_grad[*ipar];
}
// Add column to matrix
curvature->add_to_column(jpar, values, inx, ndev);
} // endfor: looped over columns
} // endfor: iterated over all events
// Free temporary memory
if (values != NULL) delete [] values;
if (inx != NULL) delete [] inx;
// Return
return value;
}
/*==========================================================================
= =
= Model gradient methods =
= =
==========================================================================*/
/***********************************************************************//**
* @brief Model function evaluation for gradient computation
*
* @param[in] x Function value.
***************************************************************************/
double GObservation::model_func::eval(const double& x)
{
// Set value
m_par->factor_value(x);
// Compute model value
double value = m_model->eval(*m_event, *m_parent);
// Return value
return value;
}
/*==========================================================================
= =
= Npred computation methods =
= =
==========================================================================*/
/***********************************************************************//**
* @brief Npred function evaluation for gradient computation
*
* @param[in] x Function value.
***************************************************************************/
double GObservation::npred_func::eval(const double& x)
{
// Set value
m_par->factor_value(x);
// Compute Npred value
double npred = m_parent->npred(*m_model);
// Return value
return npred;
}
/***********************************************************************//**
* @brief Integration kernel for npred() method
*
* @param[in] x Function value.
***************************************************************************/
double GObservation::npred_kern::eval(const double& x)
{
// Convert argument in native reference in seconds
GTime time;
time.secs(x);
// Return value
return (m_parent->npred_spec(*m_model, time));
}
/***********************************************************************//**
* @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}(t') = \int_{E_{\rm bounds}} \int_{\rm ROI}
* P(p',E',t') \, dp' \, dE'
* \f]
*
* of 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]
*
* where
* \f$S(p,E,t)\f$ is the source model,
* \f$R(p',E',t'|p,E,t)\f$ is the instrument response function,
* \f$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$p\f$ is the true photon arrival direction,
* \f$E\f$ is the true photon energy, and
* \f$t\f$ is the true photon arrival time.
*
* This method performs the energy intergration over the energy boundaries
* \f$E_{\rm bounds}\f$.
***************************************************************************/
double GObservation::npred_spec(const GModel& model,
const GTime& obsTime) const
{
// Set number of iterations for Romberg integration.
static const int iter = 8;
// Initialise result
double result = 0.0;
// Get energy boundaries
GEbounds ebounds = events()->ebounds();
// Loop over energy 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
GObservation::npred_spec_kern integrand(this, &model, &obsTime);
GIntegral integral(&integrand);
// Set number of iterations
integral.fixed_iter(iter);
// Do Romberg integration
#if defined(G_LN_ENERGY_INT)
emin = std::log(emin);
emax = std::log(emax);
#endif
result += integral.romberg(emin, emax);
} // endif: energy interval was valid
} // endfor: looped over energy boundaries
// Compile option: Check for NaN/Inf
#if defined(G_NAN_CHECK)
if (gammalib::is_notanumber(result) || gammalib::is_infinite(result)) {
std::cout << "*** ERROR: GObservation::npred_spec:";
std::cout << " NaN/Inf encountered";
std::cout << " (result=" << result;
std::cout << ")" << std::endl;
}
#endif
// Return result
return result;
}
/***********************************************************************//**
* @brief Integration kernel for npred_spec() method
*
* @param[in] x Function value.
*
* This method implements the integration kernel needed for the npred_spec()
* method. If G_LN_ENERGY_INT is defined the energy integration is done
* logarithmically, i.e. @p x is given in ln(energy) instead of energy.
***************************************************************************/
double GObservation::npred_spec_kern::eval(const double& x)
{
// Set energy
GEnergy eng;
#if defined(G_LN_ENERGY_INT)
double expx = std::exp(x);
eng.MeV(expx);
#else
eng.MeV(x);
#endif
// Get function value
double value = m_model->npred(eng, *m_time, *m_parent);
// Save value if needed
#if defined(G_NAN_CHECK)
double value_out = value;
#endif
// Correct for variable substitution
#if defined(G_LN_ENERGY_INT)
value *= expx;
#endif
// Compile option: Check for NaN
#if defined(G_NAN_CHECK)
if (gammalib::is_notanumber(value) || gammalib::is_infinite(value)) {
std::cout << "*** ERROR: GObservation::npred_spec_kern::eval";
std::cout << "(x=" << x << "): ";
std::cout << " NaN/Inf encountered";
std::cout << " (value=" << value;
std::cout << " (value_out=" << value_out;
#if defined(G_LN_ENERGY_INT)
std::cout << " exp(x)=" << expx;
#endif
std::cout << ")" << std::endl;
}
#endif
// Return value
return value;
}