25 from cscripts
import obsutils
26 from cscripts
import modutils
27 from cscripts
import mputils
35 Computes the CTA sensitivity
37 This class computes the CTA sensitivity for a number of energy bins using
38 ctlike. Spectra are fitted in narrow energy bins to simulated data,
39 and the flux level is determined that leads to a particular significance.
41 The significance is determined using the Test Statistic, defined as twice
42 the likelihood difference between fitting with and without the test source.
51 self._init_csobservation(self.__class__.__name__, ctools.__version__, argv)
71 Extend ctools.csobservation getstate method to include some members
79 state = {
'base' : ctools.csobservation.__getstate__(self),
96 Extend ctools.csobservation setstate method to include some members
103 ctools.csobservation.__setstate__(self, state[
'base'])
106 self.
_fits = state[
'fits']
108 self.
_ra = state[
'ra']
109 self.
_dec = state[
'dec']
112 self.
_seed = state[
'seed']
122 Get user parameters from parfile
125 if self.obs().size() == 0:
126 self.obs(self.
_set_obs(self[
'emin'].real(), self[
'emax'].real()))
129 self._set_obs_statistic(gammalib.toupper(self[
'statistic'].string()))
132 if self.obs().models().size() == 0:
133 self.obs().models(self[
'inmodel'].filename())
139 emin = self[
'emin'].real()
140 emax = self[
'emax'].real()
141 bins = self[
'bins'].integer()
144 enumbins = self[
'enumbins'].integer()
145 if not enumbins == 0:
146 self[
'npix'].integer()
151 self[
'max_iter'].integer()
152 self[
'type'].string()
153 self[
'edisp'].boolean()
154 self[
'debug'].boolean()
155 self[
'mincounts'].integer()
158 self.
_seed = self[
'seed'].integer()
161 self.
_ebounds = gammalib.GEbounds(bins,
162 gammalib.GEnergy(emin,
'TeV'),
163 gammalib.GEnergy(emax,
'TeV'))
166 if self._read_ahead():
167 self[
'outfile'].filename()
170 self._log_parameters(gammalib.TERSE)
180 Compute median value for an array
184 array : list of floats
190 Median value of array
206 median = array[int(n/2)]
208 median = (array[int((n-1)/2)] + array[int(n/2)]) / 2.0
215 Set source name and position
218 self.
_srcname = self[
'srcname'].string()
222 source = self.obs().models()[self.
_srcname]
223 if source.has_par(
'RA')
and source.has_par(
'DEC'):
224 self.
_ra = source[
'RA'].value()
225 self.
_dec = source[
'DEC'].value()
226 elif source.has_par(
'GLON')
and source.has_par(
'GLAT'):
227 glon = source[
'GLON'].value()
228 glat = source[
'GLAT'].value()
229 srcdir = gammalib.GSkyDir()
230 srcdir.lb_deg(glon, glat)
231 self.
_ra = srcdir.ra_deg()
232 self.
_dec = srcdir.dec_deg()
242 Set an observation container
253 obs : `~gammalib.GObservations`
254 Observation container
257 if self[
'inobs'].is_valid():
258 obs = self._get_observations()
266 models = gammalib.GModels(self[
'inmodel'].filename())
269 source = models[self[
'srcname'].string()]
273 pntdir = gammalib.GSkyDir()
274 if source.has_par(
'RA')
and source.has_par(
'DEC'):
275 pntdir.radec_deg(source[
'RA'].value(), source[
'DEC'].value())
276 elif source.has_par(
'GLON')
and source.has_par(
'GLAT'):
277 pntdir.lb_deg(source[
'GLON'].value(), source[
'GLAT'].value())
279 pntdir.radec_deg(0.0, 0.0)
282 instrument = self[
'instrument'].string()
283 caldb = self[
'caldb'].string()
284 irf = self[
'irf'].string()
285 deadc = self[
'deadc'].real()
286 duration = self[
'duration'].real()
287 rad = self[
'rad'].real()
288 offset = self[
'offset'].real()
291 pntdir.rotate_deg(0.0, offset)
294 obs = gammalib.GObservations()
297 run = obsutils.set_obs(pntdir, instrument=instrument, caldb=caldb, irf=irf,
298 duration=duration, deadc=deadc,
299 emin=emin, emax=emax, rad=rad)
309 Set energy boundaries for all observations in container
313 emin : `~gammalib.GEnergy`
315 emax : `~gammalib.GEnergy`
319 for i, obs
in enumerate(self.obs()):
325 obs_emin = obs_ebounds.emin()
326 obs_emax = obs_ebounds.emax()
330 if obs_emin <= emin
and obs_emax >= emax:
331 ebounds = gammalib.GEbounds(emin, emax)
336 elif emax < obs_emin
or emin > obs_emax:
337 e0 = gammalib.GEnergy(0.0,
'TeV')
338 ebounds = gammalib.GEbounds(e0, e0)
345 set_emin = max(emin, obs_emin)
346 set_emax = min(emax, obs_emax)
349 ebounds = gammalib.GEbounds(set_emin, set_emax)
352 obs.events().ebounds(ebounds)
359 Return Crab photon flux in a given energy interval
363 emin : `~gammalib.GEnergy`
365 emax : `~gammalib.GEnergy`
371 Crab photon flux in specified energy interval (ph/cm2/s)
374 crab = gammalib.GModelSpectralPlaw(5.7e-16, -2.48,
375 gammalib.GEnergy(0.3,
'TeV'))
378 flux = crab.flux(emin, emax)
385 Determine sensitivity for given observations
389 emin : `~gammalib.GEnergy`
390 Minimum energy for fitting and flux computation
391 emax : `~gammalib.GEnergy`
392 Maximum energy for fitting and flux computation
393 test_model : `~gammalib.GModels`
405 ts_thres = self[
'sigma'].real() * self[
'sigma'].real()
406 max_iter = self[
'max_iter'].integer()
407 enumbins = self[
'enumbins'].integer()
408 if not enumbins == 0:
409 npix = self[
'npix'].integer()
410 binsz = self[
'binsz'].real()
417 edisp_orig = self[
'edisp'].boolean()
418 self[
'edisp'] =
False
421 ratio_precision = 0.05
424 self._log_string(gammalib.TERSE,
'')
425 self._log_header2(gammalib.TERSE,
'Energies: '+str(emin)+
' - '+str(emax))
431 e_mean = math.sqrt(emin.TeV()*emax.TeV())
432 loge = math.log10(e_mean)
433 erg_mean = e_mean * tev2erg
434 energy = gammalib.GEnergy(e_mean,
'TeV')
439 if test_model[self.
_srcname].spatial().classname() ==
'GModelSpatialDiffuseCube':
441 if test_model[self.
_srcname].spatial().spectrum().nodes() == 0:
442 test_model[self.
_srcname].spatial().set_mc_cone(gammalib.GSkyDir(),180.0)
447 src_flux = test_model[self.
_srcname].spectral().eval(energy) * \
448 test_model[self.
_srcname].spatial().spectrum().flux(emin, emax)
450 src_flux = test_model[self.
_srcname].spectral().flux(emin, emax)
455 crab_unit = crab_flux/src_flux
458 self._log_header3(gammalib.TERSE,
'Initial parameters')
459 self._log_value(gammalib.TERSE,
'Mapcube model', str(mapcube))
460 self._log_value(gammalib.TERSE,
'Crab flux', str(crab_flux)+
' ph/cm2/s')
461 self._log_value(gammalib.TERSE,
'Source model flux', str(src_flux)+
' ph/cm2/s')
462 self._log_value(gammalib.TERSE,
'Crab unit factor', crab_unit)
474 self._log_header3(gammalib.TERSE,
'Iterations')
486 self._log_header2(gammalib.EXPLICIT,
'Iteration '+str(iterations))
493 sim = obsutils.sim(self.obs(), nbins=enumbins, seed=self.
_seed,
494 binsz=binsz, npix=npix,
496 debug=self[
'debug'].boolean(),
497 edisp=self[
'edisp'].boolean(),
501 nevents = sim.nobserved()
504 self._log_header3(gammalib.EXPLICIT,
'Simulation')
505 self._log_value(gammalib.EXPLICIT,
'Number of simulated events', nevents)
509 fit = ctools.ctlike(sim)
510 fit[
'edisp'] = self[
'edisp'].boolean()
512 fit[
'debug'] = self[
'debug'].boolean()
513 fit[
'chatter'] = self[
'chatter'].integer()
517 logL = fit.opt().value()
518 npred = fit.obs().npred()
519 models = fit.obs().models()
524 prefactor = modutils.normalisation_parameter(source)
525 crab_flux = prefactor.value() / crab_prefactor
530 diff_flux = source.spectral().eval(energy)
535 photon_flux = diff_flux*source.spatial().spectrum().flux(emin, emax)
536 energy_flux = diff_flux*source.spatial().spectrum().eflux(emin, emax)
538 photon_flux = source.spectral().flux(emin, emax)
539 energy_flux = source.spectral().eflux(emin, emax)
548 sensitivity = diff_flux*e_mean*erg_mean*1.0e6
550 sensitivity *= source.spatial().spectrum().eval(energy)
553 name =
'Iteration %d' % iterations
554 value = (
'TS=%10.4f Sim=%9.4f mCrab Fit=%9.4f mCrab '
555 'Sens=%e erg/cm2/s' %
556 (ts, test_crab_flux*1000.0, crab_flux*1000.0, sensitivity))
557 self._log_value(gammalib.TERSE, name, value)
563 if (iterations >= max_iter):
564 self._log_string(gammalib.TERSE,
565 ' Test ended after %d iterations.' % max_iter)
569 test_crab_flux *= 3.0
572 self._log_string(gammalib.EXPLICIT,
573 'Non positive TS, increase test flux and start over.')
579 result = {
'ts': ts,
'crab_flux': crab_flux,
580 'photon_flux': photon_flux,
581 'energy_flux': energy_flux}
582 results.append(result)
588 correct = math.sqrt(ts_thres / ts)
591 pred_crab_flux, regcoeff = self.
_predict_flux(results, ts_thres)
592 correct = pred_crab_flux / crab_flux
594 self._log_value(gammalib.TERSE,
'Skipping failed regression',
595 'Retain simple TS scaling relation')
598 crab_flux = correct * crab_flux
599 photon_flux = correct * photon_flux
600 energy_flux = correct * energy_flux
601 sensitivity = correct * sensitivity
606 if test_crab_flux > 0:
610 ratio = crab_flux/test_crab_flux
614 if ratio > 1.0-ratio_precision
and \
615 ratio < 1.0+ratio_precision:
616 value = (
'TS=%10.4f Sim=%9.4f mCrab '
617 ' Sens=%e erg/cm2/s' %
618 (ts, crab_flux*1000.0, sensitivity))
619 self._log_value(gammalib.TERSE,
'Converged result', value)
620 self._log_value(gammalib.TERSE,
'Converged flux ratio', ratio)
621 self._log_value(gammalib.TERSE,
'Regression coefficient',
627 if edisp_orig
and not self[
'edisp'].boolean():
633 self._log_value(gammalib.TERSE,
'Converged result',
634 'Now use energy dispersion after '
635 'initial convergence without it')
643 self._log_value(gammalib.TERSE,
'Not converged',
'Flux is zero')
647 test_crab_flux = crab_flux
650 if (iterations >= max_iter):
651 self._log_string(gammalib.TERSE,
652 ' Test ended after %d iterations.' % max_iter)
657 self._log_header3(gammalib.TERSE,
'Iterations for source counts cuts')
660 self[
'edisp'] = edisp_orig
664 for iterations
in range(max_iter):
670 n_bck_evts=n_bck_evts)
673 name =
'Iteration %d' % iterations
674 value =
'Fit=%9.4f mCrab Sens=%e erg/cm2/s' % (crab_flux*1000.0, sensitivity)
675 self._log_value(gammalib.TERSE, name, value)
682 correct = 1.0 + ratio_precision
683 crab_flux = correct * crab_flux
684 photon_flux = correct * photon_flux
685 energy_flux = correct * energy_flux
686 sensitivity = correct * sensitivity
692 self._log_header3(gammalib.TERSE,
'Fit results')
693 self._log_value(gammalib.TERSE,
'Photon flux',
694 str(photon_flux)+
' ph/cm2/s')
695 self._log_value(gammalib.TERSE,
'Energy flux',
696 str(energy_flux)+
' erg/cm2/s')
697 self._log_value(gammalib.TERSE,
'Crab flux',
698 str(crab_flux*1000.0)+
' mCrab')
699 self._log_value(gammalib.TERSE,
'Differential sensitivity',
700 str(sensitivity)+
' erg/cm2/s')
701 self._log_value(gammalib.TERSE,
'Number of simulated events', nevents)
702 self._log_header3(gammalib.TERSE,
'Test source model fitting')
703 self._log_value(gammalib.TERSE,
'log likelihood', logL)
704 self._log_value(gammalib.TERSE,
'Number of predicted events', npred)
706 self._log_value(gammalib.TERSE,
'Model', model.name())
708 self._log_string(gammalib.TERSE, str(par))
711 for i, obs
in enumerate(self.obs()):
715 result = {
'loge': loge,
'emin': emin.TeV(),
'emax': emax.TeV(), \
716 'crab_flux': crab_flux,
'photon_flux': photon_flux, \
717 'energy_flux': energy_flux, \
718 'sensitivity': sensitivity,
'regcoeff': regcoeff, \
719 'nevents': nevents,
'npred': npred}
726 Create a copy of the test models, set the normalisation parameter
727 of the test source in the models, and append the models to the observation.
731 test_model : `~gammalib.GModels`
735 test_crab_flux : float
736 Test flux in Crab units (100 mCrab)
740 crab_prefactor : float
741 the prefactor that corresponds to a flux of 1 Crab.
744 models = test_model.copy()
745 prefactor = modutils.normalisation_parameter(models[self.
_srcname])
746 crab_prefactor = prefactor.value() * crab_unit
748 min_pref = prefactor.min() * (1.0 + val_margin)
749 max_pref = prefactor.max() * (1.0 - val_margin)
750 val_now = crab_prefactor * test_crab_flux
753 if val_now < min_pref
or val_now > max_pref:
759 val_now = max(val_now, min_pref)
760 val_now = min(val_now, max_pref)
761 crab_prefactor = val_now / test_crab_flux
764 self._log_value(gammalib.EXPLICIT,
'Prefactor range',
765 '['+str(min_pref)+
','+str(max_pref)+
']')
766 self._log_value(gammalib.EXPLICIT,
'Initial Prefactor', val_old)
767 self._log_value(gammalib.EXPLICIT,
'Updated Prefactor', val_now)
770 prefactor.value(val_now)
773 self.obs().models(models)
776 return crab_prefactor
784 obs : `~gammalib.GObservations`
785 Observation container
787 Simulation selection radius (degrees)
789 Number of energy bins
799 for n_sim
in range(15):
805 sim = obsutils.sim(obs, nbins=enumbins, seed=self.
_seed, binsz=binsz,
807 debug=self[
'debug'].boolean(),
808 edisp=self[
'edisp'].boolean(), nthreads=1)
815 select = ctools.ctselect(sim)
817 select[
'emin'] =
'INDEF'
818 select[
'tmin'] =
'INDEF'
822 n_sim_evts.append(select.obs().nobserved())
826 n_sim_evts.append(sim.nobserved())
829 median = self.
_median(n_sim_evts)
833 name =
'Median source events'
835 name =
'Median background events'
836 self._log_value(gammalib.EXPLICIT, name, median)
843 Return the number of excess events for the source model, compared
849 Number of bins for the observation simulation
851 Bin size for the observation simulation
853 Pixel size for the observation simulation
855 Number of background counts from previous call
860 Signals that the source passes the minimal excess criteria
862 Number of background counts
869 mincounts = self[
'mincounts'].integer()
870 bkgexcess = self[
'bkgexcess'].real()
871 bkgrad = self[
'bkgrad'].real()
874 if mincounts > 0
or bkgexcess > 0.0:
877 models = self.obs().models()
878 src_model = gammalib.GModels()
879 bck_models = gammalib.GModels()
882 src_model.append(model)
884 bck_models.append(model)
887 src_obs = self.obs().copy()
888 bck_obs = self.obs().copy()
889 src_obs.models(src_model)
890 bck_obs.models(bck_models)
898 if bkgexcess > 0.0
and n_bck_evts ==
None:
902 min_bkg_events = n_bck_evts * bkgexcess
903 has_min_evts = n_src_evts >= mincounts
904 is_above_bck = n_src_evts >= min_bkg_events
905 pass_evt_cut = has_min_evts
and is_above_bck
908 self._log_value(gammalib.EXPLICIT,
'Pass minimum counts cut', has_min_evts)
909 self._log_value(gammalib.EXPLICIT,
'Pass background cut', is_above_bck)
910 self._log_value(gammalib.EXPLICIT,
'Pass event cut', pass_evt_cut)
911 self._log_value(gammalib.EXPLICIT,
'Minimum counts threshold', mincounts)
912 self._log_value(gammalib.EXPLICIT,
'Background threshold', min_bkg_events)
913 self._log_value(gammalib.EXPLICIT,
'Source events', n_src_evts)
914 self._log_value(gammalib.EXPLICIT,
'Background events', n_bck_evts)
917 return pass_evt_cut, n_bck_evts
921 Predict Crab flux for a given TS value
923 The Crab flux at a given Test Statistic value is predicted by doing a
924 linear regression of the log(TS) and log(crab_flux) values in a results
927 See https://en.wikipedia.org/wiki/Simple_linear_regression
931 results : list of dict
938 crab_flux_prediction, regcoeff : tuple of float
939 Predicted Crab flux and regression coefficient
947 for result
in results:
948 x = math.log(float(result[
'ts']))
949 y = math.log(float(result[
'crab_flux']))
955 norm = 1.0 / float(len(results))
963 rxy_norm = (mean_xx - mean_x * mean_x) * (mean_yy - mean_y * mean_y)
967 rxy_norm = math.sqrt(rxy_norm)
968 rxy = (mean_xy - mean_x * mean_y) / rxy_norm
974 for result
in results:
975 x = math.log(float(result[
'ts']))
976 y = math.log(float(result[
'crab_flux']))
977 beta_nom += (x - mean_x) * (y - mean_y)
978 beta_denom += (x - mean_x) * (x - mean_x)
979 beta = beta_nom / beta_denom
982 alpha = mean_y - beta * mean_x
985 log_ts_thres = math.log(ts)
986 crab_flux_prediction = math.exp(alpha + beta * log_ts_thres)
989 return (crab_flux_prediction, regcoeff)
993 Determines sensivity in energy bin
1006 sensitivity_type = self[
'type'].string()
1009 if sensitivity_type ==
'Differential':
1010 emin = self._ebounds.emin(ieng)
1011 emax = self._ebounds.emax(ieng)
1012 elif sensitivity_type ==
'Integral':
1013 emin = self._ebounds.emin(ieng)
1014 emax = self._ebounds.emax()
1016 msg = (
'Invalid sensitivity type "%s" encountered. Either '
1017 'specify "Differential" or "Integral".' %
1019 raise RuntimeError(msg)
1029 Create FITS file from results
1033 results : list of dict
1034 List of result dictionaries
1037 nrows = len(results)
1038 e_mean = gammalib.GFitsTableDoubleCol(
'E_MEAN', nrows)
1039 e_min = gammalib.GFitsTableDoubleCol(
'E_MIN', nrows)
1040 e_max = gammalib.GFitsTableDoubleCol(
'E_MAX', nrows)
1041 flux_crab = gammalib.GFitsTableDoubleCol(
'FLUX_CRAB', nrows)
1042 flux_photon = gammalib.GFitsTableDoubleCol(
'FLUX_PHOTON', nrows)
1043 flux_energy = gammalib.GFitsTableDoubleCol(
'FLUX_ENERGY', nrows)
1044 sensitivity = gammalib.GFitsTableDoubleCol(
'SENSITIVITY', nrows)
1045 regcoeff = gammalib.GFitsTableDoubleCol(
'REGRESSION_COEFF', nrows)
1046 nevents = gammalib.GFitsTableDoubleCol(
'NEVENTS', nrows)
1047 npred = gammalib.GFitsTableDoubleCol(
'NPRED', nrows)
1052 flux_photon.unit(
'ph/cm2/s')
1053 flux_energy.unit(
'erg/cm2/s')
1054 sensitivity.unit(
'erg/cm2/s')
1056 nevents.unit(
'counts')
1057 npred.unit(
'counts')
1060 for i, result
in enumerate(results):
1061 e_mean[i] = math.pow(10.0, result[
'loge'])
1062 e_min[i] = result[
'emin']
1063 e_max[i] = result[
'emax']
1064 flux_crab[i] = result[
'crab_flux']
1065 flux_photon[i] = result[
'photon_flux']
1066 flux_energy[i] = result[
'energy_flux']
1067 sensitivity[i] = result[
'sensitivity']
1068 regcoeff[i] = result[
'regcoeff']
1069 nevents[i] = result[
'nevents']
1070 npred[i] = result[
'npred']
1073 table = gammalib.GFitsBinTable(nrows)
1074 table.extname(
'SENSITIVITY')
1077 table.card(
'INSTRUME',
'CTA',
'Name of Instrument')
1078 table.card(
'TELESCOP',
'CTA',
'Name of Telescope')
1084 table.card(
'OBJECT', self.
_srcname,
'Source for which sensitivity was estimated')
1085 table.card(
'TYPE', self[
'type'].string(),
'Sensitivity type')
1086 table.card(
'SIGMA', self[
'sigma'].real(),
'[sigma] Sensitivity threshold')
1087 table.card(
'MAX_ITER', self[
'max_iter'].integer(),
'Maximum number of iterations')
1088 table.card(
'STAT', self[
'statistic'].string(),
'Optimization statistic')
1089 table.card(
'MINCOUNT', self[
'mincounts'].integer(),
'Minimum number of source counts')
1090 table.card(
'BKGEXCES', self[
'bkgexcess'].real(),
'Background uncertainty fraction')
1091 table.card(
'BKGRAD', self[
'bkgrad'].real(),
'[deg] Background radius')
1092 table.card(
'SEED', self[
'seed'].integer(),
'Seed value for random numbers')
1095 table.append(e_mean)
1098 table.append(flux_crab)
1099 table.append(flux_photon)
1100 table.append(flux_energy)
1101 table.append(sensitivity)
1102 table.append(regcoeff)
1103 table.append(nevents)
1107 self.
_fits = gammalib.GFits()
1108 self._fits.append(table)
1124 for obs
in self.obs():
1125 self._obs_ebounds.append(obs.events().ebounds().copy())
1131 self.
_models = modutils.test_source(self.obs().models(), self.
_srcname,
1135 if self.
_models[self.
_srcname].spatial().classname() ==
'GModelSpatialDiffuseCube':
1136 self.
_models[self.
_srcname].spatial().set_mc_cone(gammalib.GSkyDir(),180.0)
1139 self._log_observations(gammalib.NORMAL, self.obs(),
'Input observation')
1142 self._log_models(gammalib.NORMAL, self.
_models,
'Input model')
1145 self._log_header1(gammalib.TERSE,
'Sensitivity determination')
1146 self._log_value(gammalib.TERSE,
'Type', self[
'type'].string())
1149 if self.
_nthreads > 1
and self._ebounds.size() > 1:
1152 args = [(self,
'_e_bin', i)
1153 for i
in range(self._ebounds.size())]
1154 poolresults = mputils.process(self.
_nthreads, mputils.mpfunc, args)
1157 for ieng
in range(self._ebounds.size()):
1158 results.append(poolresults[ieng][0])
1159 self._log_string(gammalib.TERSE, poolresults[ieng][1][
'log'],
False)
1163 for ieng
in range(self._ebounds.size()):
1166 result = self.
_e_bin(ieng)
1169 results.append(result)
1179 Save sensitivity FITS file
1182 self._log_header1(gammalib.TERSE,
'Save sensitivity curve')
1185 if self.
_fits !=
None:
1188 outfile = self[
'outfile'].filename()
1191 self._log_value(gammalib.NORMAL,
'Sensitivity file', outfile.url())
1194 self._fits.saveto(outfile, self[
'clobber'].boolean())
1201 Return sensitivity FITS file
1204 FITS file containing sensitivity curve
1213 if __name__ ==
'__main__':