Commit 47cd6656e630a3e062e68b2bd1518587734f00d6

Authored by Jürgen Knödlseder
2 parents edd8ccb7 c6f557e5

Merge branch '2289-add-csresspec' into integration

# Conflicts:
#	doc/source/users/tutorials/beyond/index.rst
cscripts/Makefile.am
... ... @@ -37,6 +37,7 @@ pythonscripts = $(top_builddir)/cscripts/__init__.py \
37 37 $(srcdir)/csphasecrv.py \
38 38 $(srcdir)/cspull.py \
39 39 $(srcdir)/csresmap.py \
  40 + $(srcdir)/csresspec.py \
40 41 $(srcdir)/csroot2caldb.py \
41 42 $(srcdir)/cssens.py \
42 43 $(srcdir)/csspec.py \
... ... @@ -71,6 +72,7 @@ parfiles = $(srcdir)/cscaldb.par \
71 72 $(srcdir)/csphasecrv.par \
72 73 $(srcdir)/cspull.par \
73 74 $(srcdir)/csresmap.par \
  75 + $(srcdir)/csresspec.par \
74 76 $(srcdir)/csroot2caldb.par \
75 77 $(srcdir)/cssens.par \
76 78 $(srcdir)/csspec.par \
... ... @@ -102,6 +104,7 @@ linkscripts = cscaldb \
102 104 csphasecrv \
103 105 cspull \
104 106 csresmap \
  107 + csresspec \
105 108 csroot2caldb \
106 109 cssens \
107 110 csspec \
... ...
cscripts/__init__.py.in
1 1 # ==========================================================================
2 2 # cscripts Python module
3 3 #
4   -# Copyright (C) 2015-2017 Juergen Knoedlseder
  4 +# Copyright (C) 2015-2018 Juergen Knoedlseder
5 5 #
6 6 # This program is free software: you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
... ... @@ -40,6 +40,7 @@ __all__ = [
40 40 "csphasecrv",
41 41 "cspull",
42 42 "csresmap",
  43 + "csresspec",
43 44 "csroot2caldb",
44 45 "cssens",
45 46 "csspec",
... ... @@ -77,6 +78,7 @@ from cscripts.csphagen import csphagen
77 78 from cscripts.csphasecrv import csphasecrv
78 79 from cscripts.cspull import cspull
79 80 from cscripts.csresmap import csresmap
  81 +from cscripts.csresspec import csresspec
80 82 from cscripts.csroot2caldb import csroot2caldb
81 83 from cscripts.cssens import cssens
82 84 from cscripts.csspec import csspec
... ...
cscripts/csresmap.py
... ... @@ -19,9 +19,9 @@
19 19 #
20 20 # ==========================================================================
21 21 import sys
22   -import math
23 22 import gammalib
24 23 import ctools
  24 +from cscripts import obsutils
25 25  
26 26  
27 27 # ============== #
... ... @@ -209,67 +209,14 @@ class csresmap(ctools.csobservation):
209 209 # Get model map into GSkyMap object
210 210 modelmap = model.cube().counts().copy()
211 211  
212   - # Store counts map as residual map. Note that we need a special
  212 + # Calculate residual maps
  213 + # Note that we need a special
213 214 # construct here to avoid memory leaks. This seems to be a SWIG feature
214 215 # as SWIG creates a new object when calling binning.cube()
215   - self._resmap = countmap.copy()
216   - self._resmap.stack_maps()
  216 + countmap1 = countmap.copy()
  217 + countmap1.stack_maps()
217 218 modelmap.stack_maps()
218   -
219   - # Get residual map algorithm type
220   - algorithm = self['algorithm'].string()
221   -
222   - # Subtract maps
223   - if algorithm == 'SUB':
224   - self._resmap -= modelmap
225   -
226   - # Subtract and divide by model map
227   - elif algorithm == 'SUBDIV':
228   - self._resmap -= modelmap
229   - self._resmap /= modelmap
230   -
231   - # Subtract and divide by sqrt of model map
232   - elif algorithm == 'SUBDIVSQRT':
233   - self._resmap -= modelmap
234   - self._resmap /= modelmap.sqrt()
235   -
236   - # Calculate significance from Li&Ma derivation
237   - elif algorithm == 'SIGNIFICANCE':
238   -
239   - # Compute sign map
240   - signmap = (self._resmap - modelmap).sign()
241   -
242   - # Loop over every bin in the map
243   - for i in range(self._resmap.npix()):
244   -
245   - # If the model value > 0.0 do the computation as normal ...
246   - model_val = modelmap[i]
247   - if model_val > 0.0:
248   -
249   - # If the data value is also > 0 then compute the
250   - # significance^2 and save it to the map ...
251   - data_val = self._resmap[i]
252   - if data_val > 0.0:
253   - log_val = math.log(data_val/model_val)
254   - self._resmap[i] = (data_val*log_val) + model_val - data_val
255   -
256   - # ... otherwise compute the reduced value of the above
257   - # expression. This is necessary to avoid computing log(0).
258   - else:
259   - self._resmap[i] = model_val
260   -
261   - # ... otherwise hard-code the significance to 0
262   - else:
263   - self._resmap[i] = 0.0
264   -
265   - # Compute significance map
266   - self._resmap *= 2.0
267   - self._resmap = self._resmap.sqrt()
268   - self._resmap = self._resmap * signmap
269   -
270   - # Raise exception if algorithm is unknown
271   - else:
272   - raise TypeError('Algorithm "'+algorithm+'" not known')
  219 + self._resmap = obsutils.residuals(self,countmap1,modelmap)
273 220  
274 221 # Optionally publish map
275 222 if self['publish'].boolean():
... ...
cscripts/csresspec.par 0 → 100644
  1 +#############################################################################
  2 +# #
  3 +# CTA Science Analysis Tools #
  4 +# #
  5 +# ------------------------------------------------------------------------- #
  6 +# #
  7 +# File: csresspec.par #
  8 +# #
  9 +# Author: Luigi Tibaldo #
  10 +# luigi.tibaldo@irap.omp.eu #
  11 +# IRAP #
  12 +# #
  13 +# Purpose: Parameter file for the csresspec script #
  14 +# #
  15 +#############################################################################
  16 +#
  17 +# Input and output parameters
  18 +#============================
  19 +inobs, f, a, events.fits,,, "Input event list, counts cube, or observation definition XML file"
  20 +inmodel, f, a, $CTOOLS/share/models/crab.xml,,, "Input model definition XML file"
  21 +modcube, f, a, NONE,,, "Input model cube file (generated with ctmodel)"
  22 +expcube, f, a, NONE,,, "Input exposure cube file"
  23 +psfcube, f, a, NONE,,, "Input PSF cube file"
  24 +edispcube, f, a, NONE,,, "Input energy dispersion cube file"
  25 +bkgcube, f, a, NONE,,, "Input background cube file"
  26 +caldb, s, a, prod2,,, "Calibration database"
  27 +irf, s, a, South_0.5h,,, "Instrument response function"
  28 +edisp, b, h, no,,, "Apply energy dispersion?"
  29 +outfile, f, a, resspec.fits,,, "Output residual spectrum file"
  30 +
  31 +#
  32 +# Script parameters
  33 +#==================
  34 +statistic, s, h, DEFAULT,DEFAULT|CSTAT|WSTAT|CHI2,, "Optimization statistic"
  35 +components,b, h, no,,, "Calculate model for individual components"
  36 +ebinalg, s, h, LOG,LIN|LOG|FILE,, "Binning algorithm"
  37 +emin, r, a, 0.1,,, "Lower energy limit (TeV)"
  38 +emax, r, a, 100.0,,, "Upper energy limit (TeV)"
  39 +enumbins, i, a, 20,,, "Number of model cube energy bins"
  40 +ebinfile, f, a, NONE,,, "Name of the file containing the energy bin definition"
  41 +stack, b, a, no,,, "Stack observations"
  42 +coordsys, s, a, CEL,CEL|GAL,, "Coordinate System"
  43 +proj, s, a, CAR,AIT|AZP|CAR|GLS|MER|MOL|SFL|SIN|STG|TAN,, "Projection method"
  44 +xref, r, a, 83.63,0,360, "First coordinate of image/source region center in degrees (RA or galactic l)"
  45 +yref, r, a, 22.01,-90,90, "Second coordinate of image/source region center in degrees (DEC or galactic b)"
  46 +nxpix, i, a, 200,,, "Size of the X axis in pixels"
  47 +nypix, i, a, 200,,, "Size of the Y axis in pixels"
  48 +binsz, r, a, 0.02,,, "Pixel size (deg/pixel)"
  49 +mask, b, a, no,,, "Mask data to calculate residuals in ROI"
  50 +ra, r, a, 83.63,0,360, "RA for ROI centre (degrees)"
  51 +dec, r, a, 22.01,-90,90, "Dec for ROI centre (degrees)"
  52 +rad, r, a, 3.0,0,180, "Radius of ROI (degrees)"
  53 +regfile, f, a, "NONE",,, "Input exclusion region file in ds9 format"
  54 +algorithm, s, a, SIGNIFICANCE,SUB|SUBDIV|SUBDIVSQRT|SIGNIFICANCE,, "Residuals computation algorithm"
  55 +
  56 +#
  57 +# Standard parameters
  58 +#====================
  59 +publish, b, h, no,,, "Publish residual map on VO Hub?"
  60 +chatter, i, h, 2,0,4, "Chattiness of output"
  61 +clobber, b, h, yes,,, "Overwrite existing output files with new output files?"
  62 +debug, b, h, no,,, "Debugging mode activated"
  63 +mode, s, h, ql,,, "Mode of automatic parameters"
  64 +logfile, f, h, csresspec.log,,, "Log filename"
... ...
cscripts/csresspec.py 0 → 100755
  1 +#! /usr/bin/env python
  2 +# ==========================================================================
  3 +# Generates a residual spectrum.
  4 +#
  5 +# Copyright (C) 2017-2018 Luigi Tibaldo
  6 +#
  7 +# This program is free software: you can redistribute it and/or modify
  8 +# it under the terms of the GNU General Public License as published by
  9 +# the Free Software Foundation, either version 3 of the License, or
  10 +# (at your option) any later version.
  11 +#
  12 +# This program is distributed in the hope that it will be useful,
  13 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
  14 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15 +# GNU General Public License for more details.
  16 +#
  17 +# You should have received a copy of the GNU General Public License
  18 +# along with this program. If not, see <http://www.gnu.org/licenses/>.
  19 +#
  20 +# ==========================================================================
  21 +import gammalib
  22 +import ctools
  23 +from cscripts import obsutils
  24 +import sys
  25 +
  26 +
  27 +# =============== #
  28 +# csresspec class #
  29 +# =============== #
  30 +class csresspec(ctools.csobservation):
  31 + """
  32 + Generates a residual spectrum
  33 + """
  34 +
  35 + # Constructor
  36 + def __init__(self, *argv):
  37 + """
  38 + Constructor
  39 + """
  40 + # Initialise application by calling the appropriate class constructor
  41 + self._init_csobservation(self.__class__.__name__, ctools.__version__,
  42 + argv)
  43 +
  44 + # Initialise class members
  45 + self._use_maps = False
  46 + self._stack = False
  47 + self._mask = False
  48 + self._fits = gammalib.GFits()
  49 +
  50 + # Return
  51 + return
  52 +
  53 + # Private methods
  54 + def _get_parameters(self):
  55 + """
  56 + Get parameters from parfile and setup the observation
  57 + """
  58 + # Setup observations (require response and allow event list as well as
  59 + # counts cube)
  60 + self._setup_observations(self.obs(), True, True, True)
  61 +
  62 + # Set observation statistic
  63 + self._set_obs_statistic(gammalib.toupper(self['statistic'].string()))
  64 +
  65 + # Collect number of unbinned, binned and On/Off observations in
  66 + # observation container
  67 + n_unbinned = 0
  68 + n_binned = 0
  69 + n_onoff = 0
  70 + for obs in self.obs():
  71 + if obs.classname() == 'GCTAObservation':
  72 + if obs.eventtype() == 'CountsCube':
  73 + n_binned += 1
  74 + else:
  75 + n_unbinned += 1
  76 + elif obs.classname() == 'GCTAOnOffObservation':
  77 + n_onoff += 1
  78 + n_cta = n_unbinned + n_binned + n_onoff
  79 + n_other = self.obs().size() - n_cta
  80 +
  81 + # Query whether to compute model for individual components
  82 + components = self['components'].boolean()
  83 +
  84 + # If there is only one binned observation and no model for individual
  85 + # components is required, query for precomputed model file and set
  86 + # use_maps to True
  87 + if self.obs().size() == 1 and n_binned == 1 and not components:
  88 + modcube = self['modcube'].filename()
  89 + if modcube != 'NONE':
  90 + self._use_maps = True
  91 +
  92 + # If there are unbinned observations query the energy binning parameters
  93 + if n_unbinned != 0:
  94 + self['ebinalg'].string()
  95 + if self['ebinalg'].string() == 'FILE':
  96 + self['ebinfile'].filename()
  97 + else:
  98 + self['emin'].real()
  99 + self['emax'].real()
  100 + self['enumbins'].integer()
  101 + if n_cta > n_unbinned:
  102 + n_notunbin = n_cta - n_unbinned
  103 +
  104 + # If there is more than one observation, and observations are all
  105 + # unbinned or all onoff query user to know if they wish stacked results
  106 + if self.obs().size() > 1 and \
  107 + (n_unbinned == self.obs().size() or n_onoff == self.obs().size()):
  108 + self._stack = self['stack'].boolean()
  109 + # If we are to stack event lists query parameters for cube creation
  110 + if self._stack and n_unbinned == self.obs().size():
  111 + self['coordsys'].string()
  112 + self['proj'].string()
  113 + self['xref'].real()
  114 + self['yref'].real()
  115 + self['nxpix'].integer()
  116 + self['nypix'].integer()
  117 + self['binsz'].real()
  118 +
  119 + # If we are not using a precomputed model and no models are available
  120 + # in the observation container query input XML model file
  121 + if not self._use_maps and self.obs().models().size() == 0:
  122 + self.obs().models(self['inmodel'].filename())
  123 +
  124 + # Unless all observations are On/Off query for mask definition
  125 + if n_onoff == n_cta:
  126 + pass
  127 + else:
  128 + self._mask = self['mask'].boolean()
  129 + if self._mask:
  130 + self['ra'].real()
  131 + self['dec'].real()
  132 + self['rad'].real()
  133 + self['regfile'].filename()
  134 +
  135 + # Unless all observations are On/Off, or we are using precomputed model
  136 + # maps query whether to use energy dispersion
  137 + if n_onoff == n_cta or self._use_maps:
  138 + pass
  139 + else:
  140 + self['edisp'].boolean()
  141 +
  142 + # Query algorithm for residual computation
  143 + self['algorithm'].string()
  144 +
  145 + # Read ahead output parameters
  146 + if self._read_ahead():
  147 + self['outfile'].filename()
  148 +
  149 + # Write input parameters into logger
  150 + self._log_parameters(gammalib.TERSE)
  151 +
  152 + # Write header for observation census
  153 + self._log_header1(gammalib.TERSE, 'Observation census')
  154 +
  155 + # Log census of input observations
  156 + self._log_value(gammalib.NORMAL, 'Unbinned CTA observations', n_unbinned)
  157 + self._log_value(gammalib.NORMAL, 'Binned CTA observations', n_binned)
  158 + self._log_value(gammalib.NORMAL, 'On/off CTA observations', n_onoff)
  159 + self._log_value(gammalib.NORMAL, 'Other observations', n_other)
  160 + if n_other > 0:
  161 + msg = 'WARNING: Only CTA observation can be handled, all non-CTA ' \
  162 + + 'observations will be ignored.'
  163 + self._log_string(gammalib.TERSE, msg)
  164 +
  165 + # Log for unbinned observations
  166 + if n_unbinned != 0:
  167 + msg = ' User defined energy binning will be used for %d unbinned '\
  168 + 'observations.' % (n_unbinned)
  169 + self._log_string(gammalib.TERSE, msg)
  170 + if n_cta > n_unbinned:
  171 + msg = ' The intrinsic binning will be used for the remaining '\
  172 + '%d CTA observations.' % (n_notunbin)
  173 + self._log_string(gammalib.TERSE, msg)
  174 +
  175 + # Signal how energy dispersion is applied
  176 + if n_onoff == n_cta or self._use_maps:
  177 + msg = ' Energy dispersion is applied based on the input data/model ' \
  178 + + 'and not according to the edisp parameter'
  179 + self._log_string(gammalib.TERSE, msg)
  180 +
  181 + # Return
  182 + return
  183 +
  184 + def _stack_observations(self):
  185 + """
  186 + Stack multiple observations and replace observation container
  187 + with single stacked observation
  188 +
  189 + Returns
  190 + -------
  191 + obs : `~gammalib.GObservations'
  192 + Observation container with a single stacked observation
  193 + """
  194 + # Use first observation to determine the type and apply the
  195 + # appropriate stacking method
  196 +
  197 + # If On/Off use the GCTAOnOffObservation constructor
  198 + if self.obs()[0].classname() == 'GCTAOnOffObservation':
  199 + msg = 'Stacking %d On/Off observations.' % (self.obs().size())
  200 + self._log_string(gammalib.NORMAL, msg)
  201 + stacked = gammalib.GCTAOnOffObservation(self.obs())
  202 + stacked_obs = gammalib.GObservations()
  203 + stacked_obs.append(stacked)
  204 + stacked_obs.models(self.obs().models())
  205 +
  206 + # If event list then bin observations
  207 + elif self.obs()[0].classname() == 'GCTAObservation':
  208 + msg = 'Stacking %d event lists.' % (self.obs().size())
  209 + self._log_string(gammalib.NORMAL, msg)
  210 + stacked_obs = obsutils.get_stacked_obs(self, self.obs())
  211 +
  212 + # Return
  213 + return stacked_obs
  214 +
  215 + def _bin_evlist(self, obs):
  216 + """
  217 + Turn single event list into counts cube
  218 +
  219 + Parameters
  220 + ----------
  221 + obs : `~gammalib.GObservations()`
  222 + Observation container with single event list
  223 +
  224 + Returns
  225 + -------
  226 + obs : `~gammalib.GObservations()`
  227 + Binned observation container dict with event list ROI and energy
  228 + range information
  229 + """
  230 + # Retrieve information about ROI in event list
  231 + roi = obs[0].roi()
  232 + ra = roi.centre().dir().ra_deg()
  233 + dec = roi.centre().dir().dec_deg()
  234 + rad = roi.radius()
  235 +
  236 + # We will cover the whole ROI with 0.02 deg binning
  237 + npix = int(2.0 * rad / 0.02) + 1
  238 +
  239 + # Log binning of events
  240 + self._log_string(gammalib.EXPLICIT, 'Binning events')
  241 +
  242 + # Bin events
  243 + cntcube = ctools.ctbin(obs)
  244 + cntcube['xref'] = ra
  245 + cntcube['yref'] = dec
  246 + cntcube['binsz'] = 0.02
  247 + cntcube['nxpix'] = npix
  248 + cntcube['nypix'] = npix
  249 + cntcube['proj'] = 'TAN'
  250 + cntcube['coordsys'] = 'CEL'
  251 + cntcube['ebinalg'] = self['ebinalg'].string()
  252 + if self['ebinalg'].string() == 'FILE':
  253 + cntcube['ebinfile'] = self['ebinfile'].filename()
  254 + else:
  255 + cntcube['enumbins'] = self['enumbins'].integer()
  256 + cntcube['emin'] = self['emin'].real()
  257 + cntcube['emax'] = self['emax'].real()
  258 + cntcube.run()
  259 +
  260 + # Retrieve the binned observation container
  261 + binned_obs = cntcube.obs().copy()
  262 +
  263 + # Log response computation
  264 + self._log_string(gammalib.EXPLICIT, 'Computing binned response')
  265 +
  266 + # Compute binned response
  267 + response = obsutils.get_stacked_response(obs, ra, dec,
  268 + binsz=0.02,
  269 + nxpix=npix,
  270 + nypix=npix,
  271 + coordsys='CEL',
  272 + proj='TAN',
  273 + emin=self['emin'].real(),
  274 + emax=self['emax'].real(),
  275 + edisp=self['edisp'].boolean())
  276 +
  277 + if self['edisp'].boolean():
  278 + binned_obs[0].response(response['expcube'], response['psfcube'],
  279 + response['edispcube'], response['bkgcube'])
  280 + else:
  281 + binned_obs[0].response(response['expcube'], response['psfcube'],
  282 + response['bkgcube'])
  283 +
  284 + # Get new models
  285 + models = response['models']
  286 +
  287 + # Set models for observation container
  288 + binned_obs.models(models)
  289 +
  290 + # Check if energy boundaries provided by user extend beyond
  291 + # the content of the event list
  292 + if self['emin'].real() > obs[0].events().emin().TeV():
  293 + emin = 'INDEF'
  294 + else:
  295 + emin = obs[0].events().emin().TeV()
  296 + if self['emax'].real() < obs[0].events().emax().TeV():
  297 + emax = 'INDEF'
  298 + else:
  299 + emax = obs[0].events().emax().TeV()
  300 +
  301 + # Put ROI and E bound info in dictionary
  302 + info = {'was_list': True, 'roi_ra': ra, 'roi_dec': dec, 'roi_rad': rad,
  303 + 'emin': emin, 'emax': emax}
  304 +
  305 + # Return new oberservation container
  306 + return binned_obs, info
  307 +
  308 + def _masked_cube(self, cube, ra, dec, rad, emin='INDEF', emax='INDEF',
  309 + regfile='NONE'):
  310 + """
  311 + Mask an event cube and returns the masked cube
  312 +
  313 + Parameters
  314 + ----------
  315 + cube : `~gammalib.GCTAEventCube()`
  316 + Event cube
  317 + ra : float (str 'INDEF' for no selection on direction)
  318 + Right Ascension (deg)
  319 + dec : float (str 'INDEF' for no selection on direction)
  320 + Declination (deg)
  321 + rad : float (str 'INDEF' for no selection on direction)
  322 + Radius (deg)
  323 + emin : float (str 'INDEF' for no selection on energy)
  324 + Minimum energy (TeV)
  325 + emax : float (str 'INDEF' for no selection on energy)
  326 + Maximum energy (TeV)
  327 +
  328 + Returns
  329 + -------
  330 + cube : `~gammalib.GCTAEventCube()`
  331 + Event cube
  332 + """
  333 + # Turn cube into observation container to feed to ctcubemask
  334 + obs = gammalib.GCTAObservation()
  335 + obs.events(cube)
  336 + obs_cont = gammalib.GObservations()
  337 + obs_cont.append(obs)
  338 +
  339 + # Use ctcubemask to mask event cube pixels
  340 + cubemask = ctools.ctcubemask(obs_cont)
  341 + cubemask['ra'] = ra
  342 + cubemask['dec'] = dec
  343 + cubemask['rad'] = rad
  344 + cubemask['emin'] = emin
  345 + cubemask['emax'] = emax
  346 + cubemask['regfile'] = regfile
  347 + cubemask.run()
  348 +
  349 + # Extract copy of cube from observation container (copy is needed to
  350 + # avoid memory leaks in SWIG)
  351 + cube = cubemask.obs()[0].events().copy()
  352 +
  353 + # Return cube
  354 + return cube
  355 +
  356 + def _cube_to_spectrum(self, cube, evlist_info):
  357 + """
  358 + Derive from event cube a count spectrum
  359 +
  360 + If data come from event list use only the ROI and energy range of
  361 + the original data. Apply user defined mask if requested.
  362 +
  363 + Parameters
  364 + ----------
  365 + cube : `~gammalib.GCTAEventCube()`
  366 + Event cube
  367 + evlist_info : dict
  368 + Dictionary with information on original event list
  369 +
  370 + Returns
  371 + -------
  372 + array : `~gammalib.GNdarray()'
  373 + Counts spectrum
  374 + """
  375 + # If we started from event list mask the ROI only
  376 + # for residual computation
  377 + if evlist_info['was_list']:
  378 + msg = 'Masking ROI from original event list'
  379 + self._log_string(gammalib.EXPLICIT, msg)
  380 + cube = self._masked_cube(cube, evlist_info['roi_ra'],
  381 + evlist_info['roi_dec'],
  382 + evlist_info['roi_rad'],
  383 + emin=evlist_info['emin'],
  384 + emax=evlist_info['emax'])
  385 +
  386 + # Apply user mask
  387 + if self._mask:
  388 + msg = 'Masking ROI requested by user'
  389 + self._log_string(gammalib.EXPLICIT, msg)
  390 + cube = self._masked_cube(cube, self['ra'], self['dec'],
  391 + self['rad'],
  392 + regfile=self['regfile'])
  393 +
  394 + # Extract skymap and clip at 0 to null masked areas
  395 + counts = cube.counts().copy()
  396 + counts = counts.clip(0.)
  397 +
  398 + # Convert skymap into GNdarray count spectrum
  399 + counts = counts.counts()
  400 +
  401 + # Return
  402 + return counts
  403 +
  404 + def _residuals_table(self, obs_id, ebounds, counts, model, residuals):
  405 + """
  406 + Create a Fits Table and store counts, model, and residuals
  407 +
  408 + Parameters
  409 + ----------
  410 + obs_id : str
  411 + Observation id
  412 + ebounds : `~gammalib.GEbounds()'
  413 + Energy boundaries
  414 + counts : `~gammalib.GNdarray()'
  415 + Counts spectrum
  416 + model : `~gammalib.GNdarray()'
  417 + Model spectrum
  418 + residuals : `~gammalib.GNdarray()'
  419 + Residual spectrum
  420 +
  421 + Returns
  422 + -------
  423 + table : `~gammalib.GFitsBinTable()'
  424 + Residual spectrum as FITS binary table
  425 + """
  426 + # Create FITS table columns
  427 + nrows = ebounds.size()
  428 + energy_low = gammalib.GFitsTableDoubleCol('Emin', nrows)
  429 + energy_high = gammalib.GFitsTableDoubleCol('Emax', nrows)
  430 + counts_col = gammalib.GFitsTableDoubleCol('Counts', nrows)
  431 + model_col = gammalib.GFitsTableDoubleCol('Model', nrows)
  432 + resid_col = gammalib.GFitsTableDoubleCol('Residuals', nrows)
  433 + energy_low.unit('TeV')
  434 + energy_high.unit('TeV')
  435 +
  436 + # Fill FITS table columns
  437 + for i in range(nrows):
  438 + energy_low[i] = ebounds.emin(i).TeV()
  439 + energy_high[i] = ebounds.emax(i).TeV()
  440 + counts_col[i] = counts[i]
  441 + model_col[i] = model[i]
  442 + resid_col[i] = residuals[i]
  443 +
  444 + # Initialise FITS Table with extension set to obs id
  445 + table = gammalib.GFitsBinTable(nrows)
  446 + # If name not empty add leading blank
  447 + if obs_id != '':
  448 + obs_id = ' ' + obs_id
  449 + table.extname('RESIDUALS' + obs_id)
  450 +
  451 + # Add Header card to specify algorithm used for residual computation
  452 + table.card('ALGORITHM', self['algorithm'].string(),
  453 + 'Algorithm used for computation of residuals')
  454 +
  455 + # Append filled columns to fits table
  456 + table.append(energy_low)
  457 + table.append(energy_high)
  458 + table.append(counts_col)
  459 + table.append(model_col)
  460 + table.append(resid_col)
  461 +
  462 + # Return binary table
  463 + return table
  464 +
  465 + def _append_column(self, table, name, data):
  466 + """
  467 + Append optional column to residual table
  468 +
  469 + Parameters
  470 + ----------
  471 + table : `~gammalib.GFitsBinTable()'
  472 + FITS binary table
  473 + name : str
  474 + Column name
  475 + data : `~gammalib.GEbounds()'
  476 + Data to be filled into new column
  477 +
  478 + Returns
  479 + -------
  480 + table : `~gammalib.GFitsBinTable()'
  481 + FITS binary table
  482 + """
  483 + # Check size compatibility
  484 + if table.nrows() == data.size():
  485 + pass
  486 + # Otherwise throw error
  487 + else:
  488 + msg = 'csresspec._append_column: FITS table and data have ' \
  489 + 'incompatible size.'
  490 + raise RuntimeError(msg)
  491 +
  492 + # Create column
  493 + column = gammalib.GFitsTableDoubleCol(name, table.nrows())
  494 +
  495 + # Fill data
  496 + for i, value in enumerate(data):
  497 + column[i] = value
  498 +
  499 + # Append new column to table
  500 + table.append(column)
  501 +
  502 + # Return modified table
  503 + return table
  504 +
  505 +
  506 + # Public methods
  507 + def run(self):
  508 + """
  509 + Run the script
  510 + """
  511 + # Switch screen logging on in debug mode
  512 + if self._logDebug():
  513 + self._log.cout(True)
  514 +
  515 + # Get parameters
  516 + self._get_parameters()
  517 +
  518 + # Write observation into logger
  519 + self._log_observations(gammalib.NORMAL, self.obs(), 'Observation')
  520 +
  521 + # Stack observations if requested
  522 + if self._stack:
  523 + self._log_header1(gammalib.TERSE, 'Stacking Observations')
  524 + self.obs(self._stack_observations())
  525 +
  526 + # Log processing header
  527 + self._log_header1(gammalib.TERSE, 'Processing Observations')
  528 +
  529 + # Loop over observations and calculate residuals
  530 + for s, observation in enumerate(self.obs()):
  531 +
  532 + # Retrieve and store obs id
  533 + obs_id = observation.id()
  534 +
  535 + # If observation id is empty and there is more than one observation
  536 + # replace with incremental number
  537 + if obs_id == '' and self.obs().size() > 1:
  538 + obs_id = str(s)
  539 +
  540 + # Log processing of observation
  541 + if self.obs().size() > 1:
  542 + self._log_header2(gammalib.NORMAL,
  543 + 'Processing observation %s' % obs_id)
  544 +
  545 + # Turn into observation container and assign models
  546 + obs = gammalib.GObservations()
  547 + obs.append(observation)
  548 + obs.models(self.obs().models())
  549 +
  550 + # if 3D observation
  551 + if obs[0].classname() == 'GCTAObservation':
  552 +
  553 + # Prepare Observations
  554 +
  555 + # If already binned set the evlist_info dictionary to have
  556 + # attribute was_list False
  557 + if obs[0].eventtype() == 'CountsCube':
  558 + evlist_info = {'was_list': False}
  559 + # Otherwise bin now
  560 + else:
  561 + # we remember if we binned an event list
  562 + # so that we can mask only the ROI for residual calculation
  563 + msg = 'Setting up binned observation'
  564 + self._log_string(gammalib.NORMAL, msg)
  565 + obs, evlist_info = self._bin_evlist(obs)
  566 +
  567 + # Calculate Model and residuals
  568 + # If model cube is provided load it
  569 + if self._use_maps:
  570 + modcube = gammalib.GCTAEventCube(self['inmodel'].filename())
  571 + # Otherwise calculate it now
  572 + else:
  573 + msg = 'Computing model cube'
  574 + self._log_string(gammalib.NORMAL, msg)
  575 + modelcube = ctools.ctmodel(obs)
  576 + modelcube.run()
  577 + modcube = modelcube.cube().copy()
  578 +
  579 + # Extract cntcube for residual computation
  580 + cntcube = obs[0].events().copy()
  581 +
  582 + # Derive count spectra from cubes
  583 + msg = 'Computing counts, model, and residual spectra'
  584 + self._log_string(gammalib.NORMAL, msg)
  585 + counts = self._cube_to_spectrum(cntcube, evlist_info)
  586 + model = self._cube_to_spectrum(modcube, evlist_info)
  587 +
  588 + # Calculate residuals
  589 + residuals = obsutils.residuals(self, counts, model)
  590 +
  591 + # Extract energy bounds
  592 + ebounds = cntcube.ebounds()
  593 +
  594 + # Fill results table
  595 + msg = 'Filling residual table'
  596 + self._log_string(gammalib.NORMAL, msg)
  597 + table = self._residuals_table(obs_id, ebounds, counts, model,
  598 + residuals)
  599 +
  600 + # Calculate models of individual components if requested
  601 + if self['components'].boolean():
  602 + for component in obs.models():
  603 + self._log_value(gammalib.NORMAL,
  604 + 'Computing model for component',
  605 + component.name())
  606 + # Set model cube models to
  607 + # individual component
  608 + model_cont = gammalib.GModels()
  609 + model_cont.append(component)
  610 + modelcube.obs().models(model_cont)
  611 +
  612 + # Run model cube
  613 + modelcube.run()
  614 +
  615 + # Extract spectrum of individual component
  616 + modcube = modelcube.cube().copy()
  617 + model = self._cube_to_spectrum(modcube, evlist_info)
  618 +
  619 + # append component to table
  620 + table = self._append_column(table, component.name(),
  621 + model)
  622 +
  623 + # otherwise, if On/Off
  624 + elif obs[0].classname() == 'GCTAOnOffObservation':
  625 +
  626 + onoff = obs[0]
  627 +
  628 + # Calculate Counts, Model and residuals
  629 + msg = 'Computing counts, model, and residual spectra'
  630 + self._log_string(gammalib.NORMAL, msg)
  631 +
  632 + # On spectrum
  633 + counts = onoff.on_spec().counts_spectrum()
  634 +
  635 + # Model for On region
  636 + background = onoff.model_background(obs.models()).counts_spectrum()
  637 + alpha = onoff.on_spec().backscal_spectrum()
  638 + model = background.copy()
  639 + model *= alpha
  640 + model += onoff.model_gamma(obs.models()).counts_spectrum()
  641 +
  642 + # On Residuals
  643 + residuals = obsutils.residuals(self, counts, model)
  644 +
  645 + # Extract energy bounds
  646 + ebounds = onoff.on_spec().ebounds()
  647 +
  648 + # Fill results table
  649 + msg = 'Filling residual table'
  650 + self._log_string(gammalib.NORMAL, msg)
  651 + table = self._residuals_table(obs_id, ebounds, counts, model,
  652 + residuals)
  653 +
  654 + # Get Off spectrum and add to table
  655 + msg = 'Computing counts, model, and residual spectra for Off regions'
  656 + self._log_string(gammalib.NORMAL, msg)
  657 + counts_off = onoff.off_spec().counts_spectrum()
  658 + table = self._append_column(table, 'Counts_Off',
  659 + counts_off)
  660 +
  661 + # Add background/Off model to table
  662 + table = self._append_column(table, 'Model_Off',
  663 + background)
  664 +
  665 + # Calculate Off residuals and add to table
  666 + residuals_off = obsutils.residuals(self, counts_off, background)
  667 + table = self._append_column(table, 'Residuals_Off',
  668 + residuals_off)
  669 +
  670 + # Calculate models of individual components if requested
  671 + if self['components'].boolean():
  672 + for component in obs.models():
  673 + # If background pass
  674 + # We always add the background at the end so that
  675 + # we accommodate WSTAT for which the background is not
  676 + # mandatory in the model
  677 + if component.classname() == 'GCTAModelIrfBackground':
  678 + pass
  679 + # Otherwise calculate gamma component and append to Table
  680 + else:
  681 + self._log_value(gammalib.NORMAL,
  682 + 'Computing model for component',
  683 + component.name())
  684 + # Create observation container for individual components
  685 + model_cont = gammalib.GModels()
  686 + model_cont.append(component)
  687 + # Calculate gamma model
  688 + model = onoff.model_gamma(model_cont)
  689 + model = model.counts_spectrum()
  690 + # Append to table
  691 + table = self._append_column(table, component.name(),
  692 + model)
  693 + # Add background already calculated
  694 + self._log_value(gammalib.NORMAL,
  695 + 'Computing model for component',
  696 + 'Background')
  697 + background *= onoff.on_spec().backscal_spectrum()
  698 + table = self._append_column(table, 'Background',
  699 + background)
  700 +
  701 + # Append results table to output file
  702 + self._fits.append(table)
  703 +
  704 + # Return
  705 + return
  706 +
  707 + def save(self):
  708 + """
  709 + Save residuals
  710 + """
  711 + # Write header
  712 + self._log_header1(gammalib.TERSE, 'Save residuals')
  713 +
  714 + # Continue only if FITS file is valid
  715 + if self._fits != None:
  716 +
  717 + # Get outfile parameter
  718 + outfile = self['outfile'].filename()
  719 +
  720 + # Log file name
  721 + self._log_value(gammalib.NORMAL, 'Residuals file', outfile.url())
  722 +
  723 + # Save residuals
  724 + self._fits.saveto(outfile, self['clobber'].boolean())
  725 +
  726 + # Return
  727 + return
  728 +
  729 +
  730 +# ======================== #
  731 +# Main routine entry point #
  732 +# ======================== #
  733 +if __name__ == '__main__':
  734 +
  735 + # Create instance of application
  736 + app = csresspec(sys.argv)
  737 +
  738 + # Execute application
  739 + app.execute()
... ...
cscripts/obsutils.py
1 1 # ==========================================================================
2 2 # Utility functions for observation handling
3 3 #
4   -# Copyright (C) 2011-2017 Juergen Knoedlseder
  4 +# Copyright (C) 2011-2018 Juergen Knoedlseder
5 5 #
6 6 # This program is free software: you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
... ... @@ -829,3 +829,102 @@ def get_onoff_obs(cls, obs):
829 829  
830 830 # Return On/Off oberservation container
831 831 return onoff_obs
  832 +
  833 +
  834 +# ========================================= #
  835 +# Calculate residuals from counts and model #
  836 +# ========================================= #
  837 +def residuals(cls, counts, model):
  838 + """
  839 + Calculate residuals given counts and models, according to algorithm
  840 + specified by user.
  841 +
  842 + Can handle GSkyMap or GNdarray objects
  843 +
  844 +
  845 + Parameters
  846 + ----------
  847 + cls : `~ctools.cscript`
  848 + cscript class
  849 + counts : `~gammalib.GSkyMap/~gammalib.GNdarray'
  850 + Data counts
  851 + model : `~gammalib.GSkyMap/~gammalib.GNdarray'
  852 + Model counts
  853 +
  854 + Returns
  855 + -------
  856 + residuals : `~gammalib.GSkyMap/~gammalib.GNdarray'
  857 + Residuals
  858 + """
  859 + # Find type of objects we are manipulating and set size to iterate later
  860 +
  861 + # If GNdarray
  862 + if counts.classname() == 'GNdarray':
  863 + nelem = counts.size()
  864 + elif counts.classname() == 'GSkyMap':
  865 + nelem = counts.npix()
  866 + else:
  867 + msg = 'cscripts.obsutils.residuals only handles ' \
  868 + + 'gammalib.GNdarray or gammalib.GSkyMap objects.\n'
  869 + raise RuntimeError(msg)
  870 +
  871 + # Copy counts to initialise residuals
  872 + residuals = counts.copy()
  873 +
  874 + # Get residual map algorithm type
  875 + algorithm = cls['algorithm'].string()
  876 +
  877 + # Subtract
  878 + if algorithm == 'SUB':
  879 + residuals -= model
  880 +
  881 + # Subtract and divide by model
  882 + elif algorithm == 'SUBDIV':
  883 + residuals -= model
  884 + residuals /= model
  885 +
  886 + # Subtract and divide by sqrt of model
  887 + elif algorithm == 'SUBDIVSQRT':
  888 + residuals -= model
  889 + residuals /= model.sqrt()
  890 +
  891 + # Calculate significance from Li&Ma
  892 + elif algorithm == 'SIGNIFICANCE':
  893 +
  894 + # Compute sign
  895 + sign = (residuals - model).sign()
  896 +
  897 + # Loop over every bin
  898 + for i in range(nelem):
  899 +
  900 + # If the model value > 0.0 do the computation as normal ...
  901 + model_val = model[i]
  902 + if model_val > 0.0:
  903 +
  904 + # If the data value is also > 0 then compute the
  905 + # significance^2 and save it ...
  906 + data_val = residuals[i]
  907 + if data_val > 0.0:
  908 + log_val = math.log(data_val / model_val)
  909 + residuals[i] = (data_val * log_val) + model_val - data_val
  910 +
  911 + # ... otherwise compute the reduced value of the above
  912 + # expression. This is necessary to avoid computing log(0).
  913 + else:
  914 + residuals[i] = model_val
  915 +
  916 + # ... otherwise hard-code the significance to 0
  917 + else:
  918 + residuals[i] = 0.0
  919 +
  920 + # Compute significance map
  921 + residuals *= 2.0
  922 + residuals = residuals.sqrt()
  923 + residuals *= sign
  924 +
  925 + # Raise exception if algorithm is unknown
  926 + else:
  927 + raise TypeError('Algorithm "' + algorithm + '" not known')
  928 +
  929 + # Return
  930 + return residuals
... ...
doc/source/users/reference_manual/csresspec.rst 0 → 100644
  1 +.. _csresspec:
  2 +
  3 +csresspec
  4 +=========
  5 +
  6 +Generates residual spectrum.
  7 +
  8 +
  9 +Synopsis
  10 +--------
  11 +
  12 +This scripts generates a residual spectrum for a given model. It works for
  13 +event lists, counts cubes, On/Off observations or observation definition files.
  14 +For event lists parameters that define the spectral binning need to
  15 +be provided so that the script can bin the data internally. The model
  16 +is then convolved with the instrumental response function for the chosen
  17 +binning (either specified by the user or intrinsic to the observations)
  18 +and used for residual computation. It is possible to compute the model
  19 +contributions from individual model components.
  20 +
  21 +If starting from event lists or count cubes,
  22 +before residual computation the 3D counts and model cubes are collapsed into
  23 +spectra by summing over all spatial direction. It is possible to specify a mask
  24 +to compute counts, model, and residuals only for a region of interest.
  25 +
  26 +For On/Off observations, counts, model, and residual spectra for the Off regions
  27 +are computed as well.
  28 +
  29 +For an observation definition file, when all observations are event lists or
  30 +On/Off observations it is possible to stack them before residual computation. To
  31 +do so spatial binning parameters for event lists are required. Otherwise the
  32 +residuals are calculated separately for each observation.
  33 +
  34 +Four options exist then for residual computation:
  35 +
  36 +* the subtraction of the model from the counts (SUB)
  37 +* the subtraction and division by the model (SUBDIV)
  38 +* the subtraction and division by the square root of the model (SUBDIVSQRT)
  39 +* the computation of the significance using Li & Ma (SIGNIFICANCE)
  40 +
  41 +The counts, model, and residual spectra are written into a FITS file.
  42 +
  43 +
  44 +General parameters
  45 +------------------
  46 +
  47 +``inobs [file]``
  48 + Input event list, counts cube or observation definition XML file.
  49 +
  50 +``modcube [file]``
  51 + Input model cube file (generated with ctmodel).
  52 +
  53 +``inmodel [file]``
  54 + Input model XML file.
  55 +
  56 +``expcube [file]``
  57 + Input exposure cube file.
  58 +
  59 +``psfcube [file]``
  60 + Input PSF cube file.
  61 +
  62 +``edispcube [file]``
  63 + Input energy dispersion cube file.
  64 +
  65 +``bkgcube [file]``
  66 + Input background cube file.
  67 +
  68 +``caldb [string]``
  69 + Calibration database.
  70 +
  71 +``irf [string]``
  72 + Instrument response function.
  73 +
  74 +``(edisp = no) [boolean]``
  75 + Apply energy dispersion?
  76 +
  77 +``outfile [file]``
  78 + Output file.
  79 +
  80 +``(statistic = DEFAULT) <DEFAULT|CSTAT|WSTAT> [string]``
  81 + Optimization statistic. For On/Off observations determines how
  82 + the background counts are computed. ``DEFAULT``, i.e., ``CSTAT``,
  83 + will use the input background model. ``WSTAT`` uses the background
  84 + counts from likelihood profiling as defined for the W statistic.
  85 +
  86 +``ebinalg <FILE|LIN|LOG> [string]``
  87 + Algorithm for defining energy bins.
  88 +
  89 +``emin [real]``
  90 + Lower energy value for first energy bin (in TeV).
  91 +
  92 +``emax [real]``
  93 + Upper energy value for last energy bin (in TeV).
  94 +
  95 +``enumbins [integer]``
  96 + Number of energy bins.
  97 +
  98 +``ebinfile [file]``
  99 + Name of the file containing the energy bin definition.
  100 +
  101 +``(stack = no) [boolean]``
  102 + Stack multiple observations for residual calculation.
  103 +
  104 +``coordsys <CEL|GAL> [string]``
  105 + Coordinate system (CEL - celestial, GAL - galactic) for stacking
  106 + multiple event lists.
  107 +
  108 +``proj <AIT|AZP|CAR|GLS|MER|MOL|SFL|SIN|STG|TAN> [string]``
  109 + Projection method for stacking multiple event lists.
  110 +
  111 +``xref [real]``
  112 + Right Ascension / Galactic longitude of cube centre (J2000, in degrees)
  113 + for stacking multiple event lists.
  114 +
  115 +``yref [real]``
  116 + Declination / Galactic latitude of cube centre (J2000, in degrees) for stacking
  117 + multiple event lists.
  118 +
  119 +``nxpix [integer]``
  120 + Number of cube bins in Right Ascension or Galactic longitude for stacking
  121 + multiple event lists.
  122 +
  123 +``nypix [integer]``
  124 + Number of cube bins in Declination or Galactic latitude for stacking
  125 + multiple event lists.
  126 +
  127 +``binsz [real]``
  128 + Cube bin size (in degrees/pixel) for stacking
  129 + multiple event lists.
  130 +
  131 +``ra [real]``
  132 + Right Ascension of circular selection region centre (J2000, in degrees).
  133 +
  134 +``dec [real]``
  135 + Declination of circular selection region centre (J2000, in degrees).
  136 +
  137 +``rad [real]``
  138 + Radius of circular selection region (in degrees).
  139 +
  140 +``regfile [file]``
  141 + Input exclusion region file in ds9 format.
  142 +
  143 +``algorithm <SUB|SUBDIV|SUBDIVSQRT|SIGNIFICANCE> [string]``
  144 + Algorithm used to generate the residual map:
  145 +
  146 + ``SUB``: :math:`DATA - MODEL`
  147 +
  148 + ``SUBDIV``: :math:`(DATA - MODEL) / MODEL`
  149 +
  150 + ``SUBDIVSQRT``: :math:`(DATA - MODEL) / \sqrt{MODEL}`
  151 +
  152 + ``SIGNIFICANCE``: :math:`{\rm sign}(DATA-MODEL) \times \sqrt{ 2 \times ( DATA \times \ln \left(\frac{DATA}{MODEL} \right) + MODEL - DATA ) }`
  153 +
  154 +
  155 +Standard parameters
  156 +-------------------
  157 +
  158 +``(publish = no) [boolean]``
  159 + Specifies whether the residual map should be published on VO Hub.
  160 +
  161 +``(chatter = 2) [integer]``
  162 + Verbosity of the executable:
  163 + ``chatter = 0``: no information will be logged
  164 +
  165 + ``chatter = 1``: only errors will be logged
  166 +
  167 + ``chatter = 2``: errors and actions will be logged
  168 +
  169 + ``chatter = 3``: report about the task execution
  170 +
  171 + ``chatter = 4``: detailed report about the task execution
  172 +
  173 +``(clobber = yes) [boolean]``
  174 + Specifies whether an existing residual map file should be overwritten.
  175 +
  176 +``(debug = no) [boolean]``
  177 + Enables debug mode. In debug mode the executable will dump any log file output to the console.
  178 +
  179 +``(mode = ql) [string]``
  180 + Mode of automatic parameters (default is "ql", i.e. "query and learn").
  181 +
  182 +``(logfile = csresmap.log) [string]``
  183 + Log filename.
  184 +
  185 +
  186 +Related tools or scripts
  187 +------------------------
  188 +
  189 +None
... ...
doc/source/users/reference_manual/index.rst
... ... @@ -57,6 +57,7 @@ cscripts
57 57 csphasecrv --- Computes phase curve <csphasecrv>
58 58 cspull --- Generates pull distribution <cspull>
59 59 csresmap --- Generates residual map <csresmap>
  60 + csresspec --- Generates residual spectrum <csresspec>
60 61 csroot2caldb --- Creates a caldb entry from a ROOT file <csroot2caldb>
61 62 cssens --- Computes CTA sensitivity <cssens>
62 63 csspec --- Computes spectral points <csspec>
... ...
doc/source/users/tutorials/beyond/beyond_residual_spectrum.rst 0 → 100644
  1 +.. _sec_residual_spectrum:
  2 +
  3 +Generating a residual spectrum
  4 +~~~~~~~~~~~~~~~~~~~~~~~~~
  5 +
  6 +In the previous tutorial you have used the :ref:`csresmap` script to
  7 +inspect the spatial residuals. You can also inspect the spectral
  8 +residuals using the :ref:`csresspec` script.
  9 +
  10 +The :ref:`csresspec` script can deal with any observation type (unbinned,
  11 +binned, stacked, On/Off). For the example case with unbinned
  12 +observations the script is then used as follows:
  13 +
  14 +.. code-block:: bash
  15 +
  16 + $csresspec components=yes
  17 + Input event list, counts cube, or observation definition XML file [obs.xml]
  18 + Calibration database [prod2]
  19 + Instrument response function [South_0.5h]
  20 + Lower energy limit (TeV) [0.1]
  21 + Upper energy limit (TeV) [100.0]
  22 + Number of model cube energy bins [30]
  23 + Stack observations [yes]
  24 + Coordinate System (CEL|GAL) [CEL]
  25 + Projection method (AIT|AZP|CAR|GLS|MER|MOL|SFL|SIN|STG|TAN) [CAR]
  26 + First coordinate of image/source region center in degrees (RA or galactic l) (0-360) [83.63]
  27 + Second coordinate of image/source region center in degrees (DEC or galactic b) (-90-90) [22.01]
  28 + Size of the X axis in pixels [200] 51
  29 + Size of the Y axis in pixels [200] 51
  30 + Pixel size (deg/pixel) [0.02]
  31 + Input model definition XML file [crab_results.xml]
  32 + Mask data to calculate residuals in ROI [yes] no
  33 + Residuals computation algorithm (SUB|SUBDIV|SUBDIVSQRT|SIGNIFICANCE) [SIGNIFICANCE]
  34 + Output residual spectrum file [residual_spectrum.fits]
  35 +
  36 +In our example, the residuals will cover energies from 100 GeV to 100
  37 +TeV, in 30 energy bins. Since we have decided to stack multiple
  38 +unbinned observations we have also entered parameters to perform a
  39 +spatial binning. Note that we have defined the spatial grid such that
  40 +we will obtain the residuals in a 1 degree square region around the
  41 +Crab. Optionally, a mask can be applied to either unbinned or binned
  42 +3D observations. We have used the hidden ``components`` parameter to
  43 +include the individual model components in the results file.
  44 +
  45 +The :ref:`csresspec` script created the FITS file ``residual_spectrum.fits`` that contains
  46 +the results of the residual computation. A graphical display of the fit results is
  47 +shown below.
  48 +
  49 +.. figure:: residual_spectrum.png
  50 + :width: 600px
  51 + :align: center
  52 +
  53 + *Data, model, and residuals from the spectral fit of the Crab nebula*
  54 +
  55 +..
  56 +
  57 +
  58 +.. note::
  59 +
  60 + The figure was created by typing:
  61 +
  62 + .. code-block:: bash
  63 +
  64 + $ $CTOOLS/share/examples/python/show_spectrum.py residual_spectrum.fits
  65 +
  66 + Use the option ``-h`` to select the table to display if there are
  67 + multiple observations.
  68 +
  69 +
  70 +
  71 +
  72 +
... ...
doc/source/users/tutorials/beyond/index.rst
... ... @@ -17,6 +17,7 @@ analyses.
17 17 stacked-analysis
18 18 beyond_tsmap
19 19 beyond_residual
  20 + beyond_residual_spectrum
20 21 advanced-python-fitting
21 22 beyond_irf
22 23 beyond_model
... ...
doc/source/users/tutorials/beyond/residual_spectrum.png 0 → 100644

70.6 KB

examples/Makefile.am
1 1 #############################################################################
2 2 # Makefile for ctools examples #
3 3 # ------------------------------------------------------------------------- #
4   -# Copyright (C) 2016-2017 Juergen Knoedlseder #
  4 +# Copyright (C) 2016-2018 Juergen Knoedlseder #
5 5 # ------------------------------------------------------------------------- #
6 6 # #
7 7 # This program is free software: you can redistribute it and/or modify #
... ... @@ -28,6 +28,7 @@ scripts = README.md \
28 28 show_pha.py \
29 29 show_pull_evolution.py \
30 30 show_pull_histogram.py \
  31 + show_residuals.py \
31 32 show_response.py \
32 33 show_sensitivity.py \
33 34 show_spectrum.py \
... ...
examples/show_residuals.py 0 → 100755
  1 +#! /usr/bin/env python
  2 +# ==========================================================================
  3 +# Display residuals generated by csresspec
  4 +#
  5 +# Copyright (C) 2017-2018 Luigi Tibaldo
  6 +#
  7 +# This program is free software: you can redistribute it and/or modify
  8 +# it under the terms of the GNU General Public License as published by
  9 +# the Free Software Foundation, either version 3 of the License, or
  10 +# (at your option) any later version.
  11 +#
  12 +# This program is distributed in the hope that it will be useful,
  13 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
  14 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15 +# GNU General Public License for more details.
  16 +#
  17 +# You should have received a copy of the GNU General Public License
  18 +# along with this program. If not, see <http://www.gnu.org/licenses/>.
  19 +#
  20 +# ==========================================================================
  21 +import sys
  22 +import math
  23 +import gammalib
  24 +import cscripts
  25 +try:
  26 + import matplotlib.pyplot as plt
  27 + from matplotlib import gridspec
  28 + plt.figure()
  29 + plt.close()
  30 +except (ImportError, RuntimeError):
  31 + print('This script needs the "matplotlib" module')
  32 + sys.exit()
  33 +
  34 +
  35 +# ================================= #
  36 +# Fill counts/model/residual arrays #
  37 +# ================================= #
  38 +def fill_cmr(row, counts, model, resid, e_counts, e_resid, c_counts, c_model,
  39 + c_resid, algorithm):
  40 + """
  41 + Helper function that fills in the counts, model, residuals lists and
  42 + calculate relative errors for given table row
  43 +
  44 + Parameters
  45 + ----------
  46 + row : int
  47 + Table row index
  48 + counts : list of float
  49 + Counts
  50 + model : list of float
  51 + Model
  52 + resid : list of float
  53 + Residual
  54 + e_counts : list of float
  55 + Counts error
  56 + e_resid : list of float
  57 + Residual error
  58 + c_counts : `~gammalib.GFitsTableCol()'
  59 + Counts FITS table column
  60 + c_model : `~gammalib.GFitsTableCol()'
  61 + Model FITS table column
  62 + c_resid : `~gammalib.GFitsTableCol()'
  63 + Residual FITS table column
  64 + algorithm : str
  65 + Algorithm
  66 +
  67 + Returns
  68 + -------
  69 + counts, model, resid, e_counts, e_resid : tuple of float
  70 + Values
  71 + """
  72 + # Extract values
  73 + counts.append(c_counts.real(row))
  74 + model.append(c_model.real(row))
  75 + resid.append(c_resid.real(row))
  76 +
  77 + # Calculate count error
  78 + err = math.sqrt(c_counts.real(row))
  79 + if err == 1:
  80 + # this prevents visualization problem in matplotlib
  81 + err = 0.99
  82 + else:
  83 + pass
  84 + e_counts.append(err)
  85 +
  86 + # Calculate residual error
  87 + if algorithm == 'SUB':
  88 + e_resid.append(err)
  89 + elif algorithm == 'SUBDIV':
  90 + e_resid.append(err / c_model.real(row))
  91 + elif algorithm == 'SUBDIVSQRT':
  92 + e_resid.append(err / math.sqrt(c_model.real(row)))
  93 + elif algorithm == 'SIGNIFICANCE':
  94 + e_resid.append(1.)
  95 +
  96 + # Return tuple
  97 + return counts, model, resid, e_counts, e_resid
  98 +
  99 +
  100 +# ============= #
  101 +# Plot spectrum #
  102 +# ============= #
  103 +def plot_residuals(filename, plotfile, hdu):
  104 + """
  105 + Plot spectrum
  106 +
  107 + Parameters
  108 + ----------
  109 + filename : str
  110 + Name of spectrum FITS file
  111 + plotfile : str
  112 + Plot file name
  113 + hdu : int
  114 + Number of observation to plot
  115 + """
  116 + # Read spectrum file
  117 + fits = gammalib.GFits(filename)
  118 + table = fits.table(1 + hdu)
  119 + c_emin = table['Emin']
  120 + c_emax = table['Emax']
  121 + c_counts = table['Counts']
  122 + c_model = table['Model']
  123 + c_resid = table['Residuals']
  124 + try:
  125 + c_counts_off = table['Counts_Off']
  126 + c_model_off = table['Model_Off']
  127 + c_resid_off = table['Residuals_Off']
  128 + is_onoff = True
  129 + except:
  130 + is_onoff = False
  131 +
  132 + # Initialise arrays to be filled
  133 + ebounds = []
  134 + ed_engs = []
  135 + eu_engs = []
  136 + em_engs = []
  137 + counts = []
  138 + e_counts = []
  139 + model = []
  140 + resid = []
  141 + e_resid = []
  142 + counts_off = []
  143 + e_counts_off = []
  144 + model_off = []
  145 + resid_off = []
  146 + e_resid_off = []
  147 +
  148 + # Residual algorithm
  149 + algorithm = table.header()['ALGORITHM'].string()
  150 +
  151 + # Loop over rows of the file
  152 + nrows = table.nrows()
  153 +
  154 + # Add first energy boundary
  155 + ebounds.append(c_emin.real(0))
  156 + for row in range(nrows):
  157 +
  158 + # Boundaries
  159 + ebounds.append(c_emax.real(row))
  160 +
  161 + # Geometrical mean energy and errors
  162 + emean = math.sqrt(c_emin.real(row) * c_emax.real(row))
  163 + em_engs.append(emean)
  164 + ed_engs.append(emean - c_emin.real(row))
  165 + eu_engs.append(c_emax.real(row) - emean)
  166 +
  167 + # counts, model, residuals
  168 + counts, model, resid, e_counts, e_resid = fill_cmr(row, counts, model,
  169 + resid, e_counts,
  170 + e_resid, c_counts,
  171 + c_model, c_resid,
  172 + algorithm)
  173 + # On/off
  174 + if is_onoff:
  175 + counts_off, model_off, resid_off, e_counts_off, e_resid_off = fill_cmr(
  176 + row, counts_off,
  177 + model_off, resid_off,
  178 + e_counts_off,
  179 + e_resid_off,
  180 + c_counts_off,
  181 + c_model_off, c_resid_off,
  182 + algorithm)
  183 +
  184 + # Add model value to be compatible with plt.step
  185 + model = [model[0]] + model
  186 + if is_onoff:
  187 + model_off = [model_off[0]] + model_off
  188 +
  189 + # Plot
  190 + axarr = []
  191 + if is_onoff:
  192 + f = plt.figure(figsize=(8, 4))
  193 + gs = gridspec.GridSpec(2, 2, height_ratios=[2, 1])
  194 + gs.update(hspace=0, left=0.09, right=0.97)
  195 + axarr.append(f.add_subplot(gs[0]))
  196 + axarr.append(f.add_subplot(gs[2]))
  197 + axarr.append(f.add_subplot(gs[1], sharex=axarr[0]))
  198 + axarr.append(f.add_subplot(gs[3], sharex=axarr[1]))
  199 + else:
  200 + f = plt.figure()
  201 + gs = gridspec.GridSpec(2, 1, height_ratios=[2, 1])
  202 + gs.update(hspace=0)
  203 + axarr.append(f.add_subplot(gs[0]))
  204 + axarr.append(f.add_subplot(gs[1], sharex=axarr[0]))
  205 + plt.setp(axarr[1].get_xticklabels(), visible=False)
  206 + axarr[0].set_xscale('log')
  207 + axarr[1].set_xscale('log')
  208 + axarr[0].set_yscale('log')
  209 +
  210 + # Counts and model
  211 + axarr[0].errorbar(em_engs, counts, yerr=e_counts, xerr=[ed_engs, eu_engs],
  212 + fmt='ko', capsize=0, linewidth=2, zorder=2, label='Data')
  213 + axarr[0].step(ebounds, model, color='0.5', linewidth=2, zorder=1,
  214 + label='Model')
  215 + axarr[0].set_ylabel('Counts')
  216 +
  217 + # Residuals
  218 + axarr[1].errorbar(em_engs, resid, yerr=e_resid, xerr=[ed_engs, eu_engs],
  219 + fmt='ko', capsize=0, linewidth=2, zorder=2)
  220 + axarr[1].axhline(0, color='0.5', linestyle='--')
  221 + axarr[1].set_xlabel('Energy (TeV)')
  222 + if algorithm == 'SUB':
  223 + axarr[1].set_ylabel('Residuals (counts)')
  224 + elif algorithm == 'SUBDIV':
  225 + axarr[1].set_ylabel('Residuals (fraction)')
  226 + elif algorithm == 'SUBDIVSQRT' or algorithm == 'SIGNIFICANCE':
  227 + axarr[1].set_ylabel(r'Residuals ($\sigma$)')
  228 +
  229 + # On/Off
  230 + if is_onoff:
  231 + axarr[0].set_title('ON')
  232 + axarr[2].set_title('OFF')
  233 + plt.setp(axarr[3].get_xticklabels(), visible=False)
  234 + axarr[2].set_xscale('log')
  235 + axarr[3].set_xscale('log')
  236 + axarr[2].set_yscale('log')
  237 +
  238 + # Counts and model
  239 + axarr[2].errorbar(em_engs, counts_off, yerr=e_counts_off,
  240 + xerr=[ed_engs, eu_engs],
  241 + fmt='ko', capsize=0, linewidth=2, zorder=2,
  242 + label='Data')
  243 + axarr[2].step(ebounds, model_off, color='0.5', linewidth=2, zorder=1,
  244 + label='Model')
  245 +
  246 + # Residuals
  247 + axarr[3].errorbar(em_engs, resid_off, yerr=e_resid_off,
  248 + xerr=[ed_engs, eu_engs],
  249 + fmt='ko', capsize=0, linewidth=2, zorder=2)
  250 + axarr[3].axhline(0, color='0.5', linestyle='--')
  251 + axarr[3].set_xlabel('Energy (TeV)')
  252 +
  253 + # Add spectra of individual components
  254 + skiplist = ['Counts', 'Model', 'Residuals',
  255 + 'Counts_Off', 'Model_Off', 'Residuals_Off',
  256 + 'Emin', 'Emax']
  257 + for s in range(table.ncols()):
  258 + if table[s].name() in skiplist:
  259 + pass
  260 + else:
  261 + component = []
  262 + for row in range(nrows):
  263 + component.append(table[s].real(row))
  264 + component = [component[0]] + component
  265 + axarr[0].step(ebounds, component, zorder=0,
  266 + label=table[s].name())
  267 +
  268 + # Add legend
  269 + axarr[0].legend(loc='best')
  270 +
  271 + # Show figure
  272 + if len(plotfile) > 0:
  273 + plt.savefig(plotfile)
  274 + else:
  275 + plt.show()
  276 +
  277 + # Return
  278 + return
  279 +
  280 +
  281 +# =============== #
  282 +# Show residuals #
  283 +# =============== #
  284 +def show_residuals():
  285 + """
  286 + Show residuals
  287 + """
  288 + # Set usage string
  289 + usage = 'show_spectrum.py [-p plotfile] [-h hdu] [file]'
  290 +
  291 + # Set default options
  292 + options = [{'option': '-p', 'value': ''},
  293 + {'option': '-h', 'value': 0}]
  294 +
  295 + # Get arguments and options from command line arguments
  296 + args, options = cscripts.ioutils.get_args_options(options, usage)
  297 +
  298 + # Extract script parameters from options
  299 + plotfile = options[0]['value']
  300 + hdu = options[1]['value']
  301 +
  302 + # Show residuals
  303 + plot_residuals(args[0], plotfile, hdu)
  304 +
  305 + # Return
  306 + return
  307 +
  308 +
  309 +# ======================== #
  310 +# Main routine entry point #
  311 +# ======================== #
  312 +if __name__ == '__main__':
  313 +
  314 + # Show residuals
  315 + show_residuals()
... ...
src/ctcubemask/ctcubemask.hpp
... ... @@ -33,6 +33,7 @@
33 33 #include "GammaLib.hpp"
34 34 #include "GCTALib.hpp"
35 35 #include "ctobservation.hpp"
  36 +#include "GCTAEventCube.hpp"
36 37  
37 38 /* __Definitions _________________________________________________________ */
38 39 #define CTCUBEMASK_NAME "ctcubemask"
... ... @@ -87,5 +88,4 @@ protected:
87 88 bool m_select_energy; //!< Perform energy selection
88 89 };
89 90  
90   -
91 91 #endif /* CTCUBEMASK_HPP */
... ...
test/test_csresspec.py 0 → 100755
  1 +#! /usr/bin/env python
  2 +# ==========================================================================
  3 +# This scripts performs unit tests for the csresspec script.
  4 +#
  5 +# Copyright (C) 2017-2018 Luigi Tibaldo
  6 +#
  7 +# This program is free software: you can redistribute it and/or modify
  8 +# it under the terms of the GNU General Public License as published by
  9 +# the Free Software Foundation, either version 3 of the License, or
  10 +# (at your option) any later version.
  11 +#
  12 +# This program is distributed in the hope that it will be useful,
  13 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
  14 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15 +# GNU General Public License for more details.
  16 +#
  17 +# You should have received a copy of the GNU General Public License
  18 +# along with this program. If not, see <http://www.gnu.org/licenses/>.
  19 +#
  20 +# ==========================================================================
  21 +import gammalib
  22 +import cscripts
  23 +from testing import test
  24 +
  25 +
  26 +# =============================== #
  27 +# Test class for csresspec script #
  28 +# =============================== #
  29 +class Test(test):
  30 + """
  31 + Test class for csresspec script
  32 +
  33 + This test class makes unit tests for the csresspec script by using it
  34 + from the command line and from Python.
  35 + """
  36 +
  37 + # Constructor
  38 + def __init__(self):
  39 + """
  40 + Constructor
  41 + """
  42 + # Call base class constructor
  43 + test.__init__(self)
  44 +
  45 + # Set members
  46 + self._inonoff = self._datadir + '/onoff_obs.xml'
  47 + self._onoff_model = self._datadir + '/onoff_model.xml'
  48 +
  49 + # Return
  50 + return
  51 +
  52 + # Set test functions
  53 + def set(self):
  54 + """
  55 + Set all test functions
  56 + """
  57 + # Set test name
  58 + self.name('csresspec')
  59 +
  60 + # Append tests
  61 + self.append(self._test_cmd, 'Test csresspec on command line')
  62 + self.append(self._test_python, 'Test csresspec from Python')
  63 +
  64 + # Return
  65 + return
  66 +
  67 + # Test csresspec on command line
  68 + def _test_cmd(self):
  69 + """
  70 + Test csresspec on the command line.
  71 + """
  72 + # Set script name
  73 + csresspec = self._script('csresspec')
  74 +
  75 + # Setup csresspec command
  76 + cmd = csresspec + ' inobs="' + self._events + '"' + \
  77 + ' outfile="csresspec_cmd1.fits"' + \
  78 + ' inmodel="' + self._model + '"' + \
  79 + ' caldb="' + self._caldb + '" irf="' + self._irf + '"' + \
  80 + ' emin=0.1 emax=100.0 enumbins=20' + \
  81 + ' mask=no algorithm="SUB"' + \
  82 + ' logfile="csresspec_cmd1.log" chatter=4'
  83 +
  84 + # Check if execution of wrong command fails
  85 + self.test_assert(self._execute('command_that_does_not_exist') != 0,
  86 + 'Self test of test script')
  87 +
  88 + # Check if execution was successful
  89 + self.test_assert(self._execute(cmd) == 0,
  90 + 'Check successful execution from command line')
  91 +
  92 + # Check result file
  93 + self._check_result_file('csresspec_cmd1.fits', 20, 5)
  94 +
  95 + # Setup csresspec command
  96 + cmd = csresspec + ' inobs="event_file_that_does_not_exist.fits"' + \
  97 + ' outfile="csresspec_cmd2.fits"' + \
  98 + ' inmodel="' + self._model + '"' + \
  99 + ' caldb="' + self._caldb + '" irf="' + self._irf + '"' + \
  100 + ' emin=0.1 emax=100.0 enumbins=20' + \
  101 + ' mask=no algorithm="SUBDIVSQRT"' + \
  102 + ' logfile="csresspec_cmd2.log" debug=yes chatter=2'
  103 +
  104 + # Check if execution failed
  105 + self.test_assert(self._execute(cmd) != 0,
  106 + 'Check invalid input file when executed from command line')
  107 +
  108 + # Check csresspec --help
  109 + self._check_help(csresspec)
  110 +
  111 + # Return
  112 + return
  113 +
  114 + # Test csresspec from Python
  115 + def _test_python(self):
  116 + """
  117 + Test csresspec from Python
  118 + """
  119 + # Set-up csresspec for event list
  120 + resspec = cscripts.csresspec()
  121 + resspec['inobs'] = self._events
  122 + resspec['outfile'] = 'csresspec_py1.fits'
  123 + resspec['inmodel'] = self._model
  124 + resspec['caldb'] = self._caldb
  125 + resspec['irf'] = self._irf
  126 + resspec['emin'] = 0.1
  127 + resspec['emax'] = 100.0
  128 + resspec['enumbins'] = 20
  129 + resspec['mask'] = False
  130 + resspec['algorithm'] = 'SUB'
  131 + resspec['logfile'] = 'csresspec_py1.log'
  132 + resspec['chatter'] = 2
  133 +
  134 + # Run csresspec script
  135 + resspec.logFileOpen() # Make sure we get a log file
  136 + resspec.run()
  137 + resspec.save()
  138 +
  139 + # Check result file
  140 + self._check_result_file('csresspec_py1.fits', 20, 5)
  141 +
  142 + # Set-up csresspec for event list
  143 + # with computation of individual model components
  144 + resspec = cscripts.csresspec()
  145 + resspec['inobs'] = self._events
  146 + resspec['outfile'] = 'csresspec_py2.fits'
  147 + resspec['inmodel'] = self._model
  148 + resspec['caldb'] = self._caldb
  149 + resspec['irf'] = self._irf
  150 + resspec['emin'] = 0.1
  151 + resspec['emax'] = 100.0
  152 + resspec['enumbins'] = 20
  153 + resspec['mask'] = False
  154 + resspec['algorithm'] = 'SUBDIV'
  155 + resspec['components'] = True
  156 + resspec['logfile'] = 'csresspec_py2.log'
  157 + resspec['chatter'] = 2
  158 +
  159 + # Run csresspec script
  160 + resspec.execute()
  161 +
  162 + # Check result file
  163 + self._check_result_file('csresspec_py2.fits', 20, 7)
  164 +
  165 + # Set-up csresspec for counts cube
  166 + resspec = cscripts.csresspec()
  167 + resspec['inobs'] = self._cntcube
  168 + resspec['modcube'] = 'NONE'
  169 + resspec['expcube'] = 'NONE'
  170 + resspec['psfcube'] = 'NONE'
  171 + resspec['edispcube'] = 'NONE'
  172 + resspec['bkgcube'] = 'NONE'
  173 + resspec['caldb'] = self._caldb
  174 + resspec['irf'] = self._irf
  175 + resspec['inmodel'] = self._model
  176 + resspec['outfile'] = 'csresspec_py3.fits'
  177 + resspec['mask'] = False
  178 + resspec['algorithm'] = 'SUBDIVSQRT'
  179 + resspec['logfile'] = 'csresspec_py3.log'
  180 + resspec['chatter'] = 3
  181 +
  182 + # Run csresspec script
  183 + resspec.execute()
  184 +
  185 + # Check result file
  186 + self._check_result_file('csresspec_py3.fits', 20, 5)
  187 +
  188 + # Set-up csresspec for counts cube
  189 + # with source mask
  190 + resspec = cscripts.csresspec()
  191 + resspec['inobs'] = self._cntcube
  192 + resspec['modcube'] = 'NONE'
  193 + resspec['expcube'] = 'NONE'
  194 + resspec['psfcube'] = 'NONE'
  195 + resspec['edispcube'] = 'NONE'
  196 + resspec['bkgcube'] = 'NONE'
  197 + resspec['caldb'] = self._caldb
  198 + resspec['irf'] = self._irf
  199 + resspec['inmodel'] = self._model
  200 + resspec['outfile'] = 'csresspec_py4.fits'
  201 + resspec['mask'] = True
  202 + resspec['ra'] = 83.63
  203 + resspec['dec'] = 22.01
  204 + resspec['rad'] = 0.2
  205 + resspec['regfile'] = 'NONE'
  206 + resspec['algorithm'] = 'SUBDIVSQRT'
  207 + resspec['logfile'] = 'csresspec_py4.log'
  208 + resspec['chatter'] = 3
  209 +
  210 + # Run csresspec script
  211 + resspec.execute()
  212 +
  213 + # Check result file
  214 + self._check_result_file('csresspec_py4.fits', 20, 5)
  215 +
  216 + # Set-up csresspec for On/Off observations
  217 + resspec = cscripts.csresspec()
  218 + resspec['inobs'] = self._inonoff
  219 + resspec['inmodel'] = self._onoff_model
  220 + resspec['outfile'] = 'csresspec_py5.fits'
  221 + resspec['algorithm'] = 'SIGNIFICANCE'
  222 + resspec['stack'] = False
  223 + resspec['logfile'] = 'csresspec_py5.log'
  224 + resspec['chatter'] = 3
  225 +
  226 + # Run csresspec script
  227 + resspec.execute()
  228 +
  229 + # Check result file
  230 + self._check_result_file('csresspec_py5.fits', 20, 8, nobs=2)
  231 +
  232 + # Set-up csresspec for On/Off observations
  233 + # in stacked mode and with WSTAT background
  234 + resspec = cscripts.csresspec()
  235 + resspec['inobs'] = self._inonoff
  236 + resspec['inmodel'] = self._onoff_model
  237 + resspec['outfile'] = 'csresspec_py6.fits'
  238 + resspec['algorithm'] = 'SIGNIFICANCE'
  239 + resspec['statistic'] = 'WSTAT'
  240 + resspec['stack'] = True
  241 + resspec['logfile'] = 'csresspec_py6.log'
  242 + resspec['chatter'] = 3
  243 +
  244 + # Run csresspec script
  245 + resspec.execute()
  246 +
  247 + # Check result file
  248 + self._check_result_file('csresspec_py6.fits', 20, 8)
  249 +
  250 + # Return
  251 + return
  252 +
  253 + # Check result file
  254 + def _check_result_file(self, filename, enumbins, ncols, nobs=1):
  255 + """
  256 + Check result file
  257 + """
  258 + # Load residual file
  259 + fits = gammalib.GFits(filename)
  260 +
  261 + # Check number of HDUs = observations (+ first empty image)
  262 + self.test_value(fits.size(), 1 + nobs, 'Number of observations')
  263 +
  264 + # Check first hdu with residuals
  265 + table = fits[1]
  266 +
  267 + # Check number of rows = number of energy bins
  268 + self.test_value(table.nrows(), enumbins,
  269 + 'Check for %d rows in residuals' % enumbins)
  270 +
  271 + # Check number of columns, 2 for energy bounds + 3 for 3D or 6 for
  272 + # On/Off + 1 for each model component if components = yes
  273 + self.test_value(table.ncols(), ncols,
  274 + 'Check for %d columns in spectrum' % ncols)
  275 +
  276 + # Return
  277 + return
... ...
test/test_python_cscripts.py
... ... @@ -41,6 +41,7 @@ import test_csphagen
41 41 import test_csphasecrv
42 42 import test_cspull
43 43 import test_csresmap
  44 +import test_csresspec
44 45 import test_csroot2caldb
45 46 import test_cssens
46 47 import test_csspec
... ... @@ -133,6 +134,7 @@ def test(installed=False, debug=False):
133 134 test_csphasecrv.Test(),
134 135 test_cspull.Test(),
135 136 test_csresmap.Test(),
  137 + test_csresspec.Test(),
136 138 test_csroot2caldb.Test(),
137 139 test_cssens.Test(),
138 140 test_csspec.Test(),
... ...