Commit acab87bd6e374ea91e0826b2fd45fbddd077b671

Authored by Luigi Tibaldo
1 parent f28fae01

Added On/Off analysis

cscripts/csscs.par
... ... @@ -18,14 +18,14 @@
18 18 #============================
19 19 inobs, f, a, events.fits,,, "Input event list, counts cube, or observation definition XML file"
20 20 inmodel, f, a, $CTOOLS/share/models/crab.xml,,, "Input model definition XML file"
21   -srcnames, s, a, Crab,,, "Semicolon separated list of sources"
  21 +srcnames, s, a, Crab,,, "Semicolon-separated list of sources"
22 22 expcube, f, a, NONE,,, "Input exposure cube file"
23 23 psfcube, f, a, NONE,,, "Input PSF cube file"
24 24 edispcube, f, a, NONE,,, "Input energy dispersion cube file"
25 25 bkgcube, f, a, NONE,,, "Input background cube file"
26 26 caldb, s, a, prod2,,, "Calibration database"
27 27 irf, s, a, South_0.5h,,, "Instrument response function"
28   -inexclusion, f, h, NONE,,, "Exclusion regions"
  28 +inexclusion, f, a, NONE,,, "Exclusion regions TODO: add meaningful default"
29 29 edisp, b, h, no,,, "Apply energy dispersion?"
30 30 outfile, f, a, map.fits,,, "Output map"
31 31  
... ... @@ -39,9 +39,9 @@ yref, r, a, 22.51,-90,90, "Second coordinate of image center in degrees
39 39 nxpix, i, a, 20,,, "Size of the X axis in pixels"
40 40 nypix, i, a, 20,,, "Size of the Y axis in pixels"
41 41 binsz, r, a, 0.1,,, "Pixel size (deg/pixel)"
42   -roisz, r, a, 2.,1.42,1000, "Size of the region of interest (numbers of binsz)"
  42 +rad, r, a, 0.2,,, "Radius of the region for component separation (deg)"
43 43 statistic, s, h, DEFAULT,DEFAULT|CSTAT|WSTAT|CHI2,, "Optimization statistic"
44   -method, s, a, 3D, 3D|ONOFF,, "Analysis method"
  44 +method, s, a, UNBINNED, UNBINNED|ONOFF,, "Analysis method"
45 45 ebinalg, s, a, LOG,FILE|LIN|LOG,, "Algorithm for defining energy bins"
46 46 emin, r, a, 0.1,,, "Lower energy limit (TeV)"
47 47 emax, r, a, 100.0,,, "Upper energy limit (TeV)"
... ... @@ -49,6 +49,7 @@ enumbins, i, a, 20,,, "Number of energy bins"
49 49 bkgmethod, s, h, REFLECTED,REFLECTED,, "Method for background estimation"
50 50 srcshape, s, h, CIRCLE,CIRCLE,, "Shape of the source region"
51 51 bkgregmin, i, h, 2,1,1000, "Minimum number of background regions"
  52 +bkgregskip, i, h, 1,0,10, "Number of background regions to skip next to On region"
52 53 use_model_bkg, b, h, yes,,, "Use background model?"
53 54 maxoffset, r, h, 4.0,,, "Maximum offset of source from camera center to accept observation"
54 55 etruemin, r, h, 0.01,,, "Minimum true energy (TeV)"
... ...
cscripts/csscs.py
... ... @@ -50,8 +50,8 @@ class csscs(ctools.csobservation):
50 50  
51 51 # Initialise data members
52 52 self._nthreads = 0
53   - self._roisz = 0.
54 53 self._srcnames = []
  54 + self._method = None
55 55 self._fits = None
56 56 self._excl_reg_map = None # Exclusion region map for on/off analysis
57 57  
... ... @@ -72,7 +72,7 @@ class csscs(ctools.csobservation):
72 72 # Set pickled dictionary
73 73 state = {'base' : ctools.csobservation.__getstate__(self),
74 74 'nthreads' : self._nthreads,
75   - 'roisz' : self._roisz,
  75 + 'method' : self._method,
76 76 'srcnames' : self._srcnames,
77 77 'fits' : self._fits,
78 78 'excl_reg_map' : self._excl_reg_map}
... ... @@ -92,8 +92,8 @@ class csscs(ctools.csobservation):
92 92 # Set state
93 93 ctools.csobservation.__setstate__(self, state['base'])
94 94 self._nthreads = state['nthreads']
95   - self._roisz = state['roisz']
96 95 self._srcnames = state['srcnames']
  96 + self._method = state['method']
97 97 self._fits = state['fits']
98 98 self._excl_reg_map = state['excl_reg_map']
99 99  
... ... @@ -102,6 +102,39 @@ class csscs(ctools.csobservation):
102 102  
103 103  
104 104 # Private methods
  105 + def _get_onoff_parameters(self):
  106 + """
  107 + Get On/Off analysis parameters.
  108 + This is done here rather then in ctools.is_onoff
  109 + because the exclusion map needs to be handled differently
  110 + as an automatic parameter but queried only if not already set
  111 + and we need to verify is not empty.
  112 + Also many parameters are already queried for all methods
  113 + TODO: verify if this can be made more uniform with other scripts
  114 + """
  115 + # Exclusion map
  116 + if (self._excl_reg_map is not None) and (self._excl_reg_map.map().npix() > 0):
  117 + # Exclusion map set and is not empty
  118 + pass
  119 + elif self['inexclusion'].is_valid():
  120 + inexclusion = self['inexclusion'].filename()
  121 + self._excl_reg_map = gammalib.GSkyRegionMap(inexclusion)
  122 + else:
  123 + msg = 'csscs in On/Off mode requires input exclusion region.'
  124 + raise RuntimeError(msg)
  125 +
  126 + # Other csphagen parameters
  127 + self["enumbins"].integer()
  128 + if self["bkgmethod"].string() == "REFLECTED":
  129 + self["bkgregmin"].integer()
  130 + self['bkgregskip'].integer()
  131 + self["maxoffset"].real()
  132 + self["etruemin"].real()
  133 + self["etruemax"].real()
  134 + self["etruebins"].integer()
  135 +
  136 + return
  137 +
105 138 def _get_parameters(self):
106 139 """
107 140 Get parameters from parfile
... ... @@ -111,6 +144,9 @@ class csscs(ctools.csobservation):
111 144 if self.obs().is_empty():
112 145 self.obs(self._get_observations())
113 146  
  147 + # Set observation statistic
  148 + self._set_obs_statistic(gammalib.toupper(self['statistic'].string()))
  149 +
114 150 # Collect number of unbinned, binned and On/Off observations in
115 151 # observation container
116 152 n_unbinned = 0
... ... @@ -124,24 +160,25 @@ class csscs(ctools.csobservation):
124 160 n_unbinned += 1
125 161 elif obs.classname() == 'GCTAOnOffObservation':
126 162 n_onoff += 1
  163 + n_cta = n_unbinned + n_binned + n_onoff
  164 + n_other = self.obs().size() - n_cta
127 165  
128   - # Check that we have at least one unbinned or binned CTA observation
129   - if n_unbinned == 0 and n_binned == 0:
130   - msg = 'No unbinned or binned CTA observations found. ' \
131   - 'Please provide at least one unbinned or binned ' \
132   - 'CTA observation.'
  166 + # Check if there are On/Off or non-IACT observations
  167 + if n_onoff > 0 or n_other > 0:
  168 + msg = 'On/Off or non-CTA observations found. '\
  169 + 'csscs does not support this type of observations.'
  170 + raise RuntimeError(msg)
  171 + # Otherwise check if there is a mix of binned and unbinned
  172 + elif n_unbinned > 0 and n_binned>0:
  173 + msg = 'Mix of unbinned and binned CTA observations ' \
  174 + 'found in observation container. csscs does not ' \
  175 + 'support this mix.'
133 176 raise RuntimeError(msg)
134   -
135   - # Set observation statistic
136   - self._set_obs_statistic(gammalib.toupper(self['statistic'].string()))
137 177  
138 178 # Set models if there are none in the container
139 179 if self.obs().models().size() == 0:
140 180 self.obs().models(self['inmodel'].filename())
141 181  
142   - # Query exclusion region
143   - self["inexclusion"].query()
144   -
145 182 # Query the map definition parameters
146 183 self['xref'].real()
147 184 self['yref'].real()
... ... @@ -149,29 +186,24 @@ class csscs(ctools.csobservation):
149 186 self['proj'].string()
150 187 self['nxpix'].integer()
151 188 self['nypix'].integer()
  189 + self['binsz'].real()
152 190  
153   - # Compute the size of the ROIs for the analysis in deg
154   - self._roisz = self['binsz'].real() * self['roisz'].real()
  191 + # Radius of ROI for component separation
  192 + self['rad'].real()
155 193  
156   - # Query energy boundaries
  194 + # Energy boundaries
157 195 self["emin"].real()
158 196 self["emax"].real()
159 197  
160   - # If we have unbinned observations query the analysis method
  198 + # If we have unbinned observations query analysis method
161 199 if n_unbinned > 0:
162   - # If method is ONOFF query some csphagen parameters
163   - if self['method'].string() == 'ONOFF':
164   - pass
165   - # TODO: implement Onoff analysis
166   - # self["enumbins"].integer()
167   - # self['use_model_bkg'].boolean()
168   - # if self["bkgmethod"].string() == "REFLECTED":
169   - # self["srcshape"].string()
170   - # self["bkgregmin"].integer()
171   - # self["maxoffset"].real()
172   - # self["etruemin"].real()
173   - # self["etruemax"].real()
174   - # self["etruebins"].integer()
  200 + self._method = self['method'].string()
  201 + # If method is Onoff query csphagen parameters
  202 + if self._method == 'ONOFF':
  203 + self._get_onoff_parameters()
  204 + # Otherwise set method to binned
  205 + else:
  206 + self._method = 'BINNED'
175 207  
176 208 # Query target source names
177 209 srcnames = self['srcnames'].string()
... ... @@ -190,7 +222,27 @@ class csscs(ctools.csobservation):
190 222 msg = 'Not all target sources are present in input model.'
191 223 raise RuntimeError(msg)
192 224  
193   - # Query the hidden parameters, just in case
  225 + # For On/Off analysis check the number of gamma-ray sources in the model
  226 + if self._method == 'ONOFF':
  227 + nsources = 0
  228 + # Verify if model is of type GModelSky
  229 + for model in self.obs().models():
  230 + try:
  231 + gammalib.GModelSky(model)
  232 + nsources +=1
  233 + except:
  234 + pass
  235 + # If there are background gamma-ray sources in the model
  236 + # throw runtime error
  237 + if nsources > len(self._srcnames):
  238 + msg = 'Background gamma-ray sources found in the model. ' \
  239 + 'On/Off analysis does not support this feature.'
  240 + raise RuntimeError(msg)
  241 + # Otherwise continue
  242 + else:
  243 + pass
  244 +
  245 + # Query other hidden parameters, just in case
194 246 self['edisp'].boolean()
195 247 self['calc_ulim'].boolean()
196 248 self['calc_ts'].boolean()
... ... @@ -243,6 +295,24 @@ class csscs(ctools.csobservation):
243 295 # Log model name
244 296 self._log_header3(gammalib.EXPLICIT, model.name())
245 297  
  298 + # In On/Off mode we cannot support multiple source morphologies
  299 + # Thus, if we have more than one target we will set them all
  300 + # to isotropic
  301 + if self._method == 'ONOFF' and len(self._srcnames) > 1:
  302 + # Check if it is a source
  303 + try:
  304 + gammalib.GModelSky(model)
  305 + # In this case check if the spatial model is already isotropic
  306 + if model.spatial() == 'DiffuseIsotropic':
  307 + pass
  308 + # Otherwise change it to isotropic
  309 + else:
  310 + msg = ' Setting spatial model to diffuse isotropic'
  311 + self._log_string(gammalib.EXPLICIT, msg)
  312 + model.spatial(gammalib.GModelSpatialDiffuseConst())
  313 + except:
  314 + pass
  315 +
246 316 # Freeze all parameters except the normalization
247 317 # which is assumed to be the first spectral parameter
248 318 normpar = model.spectral()[0]
... ... @@ -298,26 +368,29 @@ class csscs(ctools.csobservation):
298 368  
299 369 Returns
300 370 -------
301   - new_obs : `gammalib.GCTAObservation`
302   - Output observation with masked cube
  371 + new_obs : `gammalib.GCTAObservations`
  372 + Output observations with masked cubes
303 373 """
304 374  
305   - # Put observation in container
306   - container = gammalib.GObservations()
307   - container.append(obs)
308   -
309 375 # Filter cube according to ROI and user energy range
310   - cubemask = ctools.ctcubemask(container)
  376 + cubemask = ctools.ctcubemask(obs)
311 377 cubemask['ra'] = ra
312 378 cubemask['dec'] = dec
313 379 cubemask['rad'] = rad
314 380 cubemask['regfile'] = 'NONE'
315 381 cubemask['emin'] = self['emin'].real()
316 382 cubemask['emax'] = self['emax'].real()
  383 +
  384 + # If chatter level is verbose and debugging is requested then
  385 + # switch also on the debug model in ctcubemask
  386 + if self._logVerbose() and self._logDebug():
  387 + cubemask['debug'] = True
  388 +
  389 + # Mask cube
317 390 cubemask.run()
318 391  
319   - # Get deep copy of filtered observation
320   - new_obs = cubemask.obs()[0].copy()
  392 + # Get deep copy of filtered observations
  393 + new_obs = cubemask.obs().copy()
321 394  
322 395 # Return
323 396 return new_obs
... ... @@ -328,23 +401,19 @@ class csscs(ctools.csobservation):
328 401  
329 402 Parameters
330 403 ----------
331   - obs : `gammalib.GCTAObservation` Input observation of type cube
  404 + obs : `gammalib.GCTAObservations` Input observations of type event list
332 405 ra : `float` R.A. (deg)
333 406 dec : `float` Dec (deg)
334 407 rad : `float` radius (deg)
335 408  
336 409 Returns
337 410 -------
338   - new_obs : `gammalib.GCTAObservation`
339   - Output observation with masked event list
  411 + new_obs : `gammalib.GCTAObservations`
  412 + Output observations with masked event lists
340 413 """
341 414  
342   - # Put observation in container
343   - container = gammalib.GObservations()
344   - container.append(obs)
345   -
346 415 # Filter event list according to ROI and user energy range
347   - select = ctools.ctselect(container)
  416 + select = ctools.ctselect(obs)
348 417 select['ra'] = ra
349 418 select['dec'] = dec
350 419 select['rad'] = rad
... ... @@ -352,40 +421,21 @@ class csscs(ctools.csobservation):
352 421 select['emax'] = self['emax'].real()
353 422 select['tmin'] = 'INDEF'
354 423 select['tmax'] = 'INDEF'
  424 +
  425 + # If chatter level is verbose and debugging is requested then
  426 + # switch also on the debug model in ctcubemask
  427 + if self._logVerbose() and self._logDebug():
  428 + select['debug'] = True
  429 +
  430 + # Select event list
355 431 select.run()
356 432  
357 433 # Get deep copy of filtered observation
358   - new_obs = select.obs()[0].copy()
  434 + new_obs = select.obs().copy()
359 435  
360 436 # Return
361 437 return new_obs
362 438  
363   - def _mask_onoff(self,obs,ra,dec,rad):
364   - """
365   - Create On/Off observation
366   - with On region matching the required mask
367   -
368   - Parameters
369   - ----------
370   - obs : `gammalib.GCTAObservation` Input observation of type cube
371   - ra : `float` R.A. (deg)
372   - dec : `float` Dec (deg)
373   - rad : `float` radius (deg)
374   -
375   - Returns
376   - -------
377   - new_obs : `gammalib.GCTAOnoffObservation`
378   - Output On/Off observation
379   - """
380   -
381   - # Put observation in container
382   - container = gammalib.GObservations()
383   - container.append(obs)
384   -
385   - onoff_obs = obsutils.get_onoff_obs(self, container, ra=ra, dec=dec, rad=rad)
386   -
387   - return onoff_obs[0]
388   -
389 439 def _mask_observations(self,ra,dec,rad):
390 440 """
391 441 Create observations restricted to circular ROI
... ... @@ -402,29 +452,16 @@ class csscs(ctools.csobservation):
402 452 Observations in circular ROI
403 453 """
404 454  
405   - # Create output observation container
406   - new_obs = gammalib.GObservations()
407   -
408   - # Loop over input observations
409   - for obs in self.obs():
410   - if obs.classname() == 'GCTAObservation':
411   - if obs.eventtype() == 'CountsCube':
412   - masked_obs = self._mask_cube(obs,ra,dec,rad)
413   - else:
414   - if self['method'].string() == '3D':
415   - masked_obs = self._mask_evlist(obs,ra,dec,rad)
416   - elif self['method'].string() == 'ONOFF':
417   - pass
418   - pass
419   - # TODO: implement Onoff analysis
420   - # masked_obs = self._mask_onoff(obs, ra, dec, rad)
421   - new_obs.append(masked_obs)
422   - # Skip On-Off and non-CTA observations
423   - else:
424   - pass
425   -
426   - # Append models to output observations
427   - new_obs.models(self.obs().models())
  455 + # Determine type of masking according
  456 + # to observation type and analysis method
  457 + if self._method == 'UNBINNED':
  458 + new_obs = self._mask_evlist(self.obs(), ra, dec, rad)
  459 + elif self._method == 'BINNED':
  460 + new_obs = self._mask_cube(self.obs(), ra, dec, rad)
  461 + elif self._method == 'ONOFF':
  462 + new_obs = obsutils.get_onoff_obs(self,self.obs(),nthreads=1,
  463 + ra = ra, dec = dec,
  464 + srcname = self._srcnames[0])
428 465  
429 466 # Return
430 467 return new_obs
... ... @@ -465,7 +502,7 @@ class csscs(ctools.csobservation):
465 502  
466 503 # Mask observations
467 504 self._log_header3(gammalib.EXPLICIT, 'Masking observations')
468   - masked_obs = self._mask_observations(ra,dec,self._roisz)
  505 + masked_obs = self._mask_observations(ra,dec,self['rad'].real())
469 506  
470 507 # Set up likelihood analysis
471 508 self._log_header3(gammalib.EXPLICIT, 'Performing fit in energy bin')
... ... @@ -488,7 +525,7 @@ class csscs(ctools.csobservation):
488 525 # ROI
489 526 centre = gammalib.GSkyDir()
490 527 centre.radec_deg(ra,dec)
491   - roi = gammalib.GSkyRegionCircle(centre,self._roisz)
  528 + roi = gammalib.GSkyRegionCircle(centre,self['rad'].real())
492 529 # Energy boundaries
493 530 emin = gammalib.GEnergy(self['emin'].real(),'TeV')
494 531 emax = gammalib.GEnergy(self['emax'].real(), 'TeV')
... ... @@ -508,7 +545,7 @@ class csscs(ctools.csobservation):
508 545 # Multiply by flux from spatial component in ROI
509 546 flux *= source.spatial().flux(roi)
510 547 # Divide by solid angle of ROI
511   - flux /= gammalib.twopi * (1 - math.cos(math.radians(self._roisz)))
  548 + flux /= gammalib.twopi * (1 - math.cos(math.radians(self['rad'].real())))
512 549 result[name]['flux'] = flux
513 550  
514 551 # Get flux error
... ... @@ -561,7 +598,7 @@ class csscs(ctools.csobservation):
561 598 # Prepare some header cards
562 599 # Flux units
563 600 fcard = gammalib.GFitsHeaderCard('BUNIT', 'ph/cm2/s/sr' , 'Photon flux')
564   - txt = 'Correlation radius %f deg' %(self._roisz)
  601 + txt = 'Correlation radius %f deg' %(self['rad'].real())
565 602 roiszcard = gammalib.GFitsHeaderCard('COMMENT', '' , txt)
566 603  
567 604 # Loop over target sources
... ... @@ -646,14 +683,6 @@ class csscs(ctools.csobservation):
646 683 for i in range(npix):
647 684 results.append(self._pixel_analysis(i))
648 685  
649   - # # Loop over pixels and extract results
650   - # results = []
651   - # for inx in range(self._create_map().npix()):
652   - #
653   - # # Analyse ROI
654   - # result = self._pixel_analysis(inx)
655   - # results.append(result)
656   -
657 686 # Fill output Fits
658 687 self._fits = self._fill_fits(results)
659 688  
... ...
cscripts/obsutils.py
... ... @@ -745,7 +745,7 @@ def get_stacked_obs(cls, obs, nthreads=0):
745 745 # ================================ #
746 746 # Get On/Off observation container #
747 747 # ================================ #
748   -def get_onoff_obs(cls, obs, nthreads=0):
  748 +def get_onoff_obs(cls, obs, nthreads=0, ra = None, dec = None, srcname = ''):
749 749 """
750 750 Create On/Off observations container from given observations
751 751  
... ... @@ -757,6 +757,12 @@ def get_onoff_obs(cls, obs, nthreads=0):
757 757 Observation container
758 758 nthreads : str, optional
759 759 Number of parallel processes for On/Off spectra computation (0=all available CPUs)
  760 + ra : float, optional
  761 + R.A. of source region centre
  762 + dec : float, optional
  763 + Dec. of source region centre
  764 + srcname : str, optional
  765 + Source name
760 766  
761 767 Returns
762 768 -------
... ... @@ -767,9 +773,8 @@ def get_onoff_obs(cls, obs, nthreads=0):
767 773 if cls._logExplicit():
768 774 cls._log.header3('Creating On/Off observations')
769 775  
770   - # Initialise inmodel, srcname, inexclusion and use_model_bkg
  776 + # Initialise inmodel, inexclusion and use_model_bkg
771 777 inmodel = 'NONE'
772   - srcname = ''
773 778 inexclusion = 'NONE'
774 779 use_model_bkg = True
775 780  
... ... @@ -777,7 +782,7 @@ def get_onoff_obs(cls, obs, nthreads=0):
777 782 if cls.has_par('inmodel') and obs.models().size() == 0:
778 783 if cls['inmodel'].is_valid():
779 784 inmodel = cls['inmodel'].value()
780   - if cls.has_par('srcname'):
  785 + if srcname == '' and cls.has_par('srcname'):
781 786 srcname = cls['srcname'].value()
782 787 if cls.has_par('inexclusion'):
783 788 if cls['inexclusion'].is_valid():
... ... @@ -795,13 +800,20 @@ def get_onoff_obs(cls, obs, nthreads=0):
795 800 phagen['enumbins'] = cls['enumbins'].integer()
796 801 phagen['ebinalg'] = 'LOG'
797 802 phagen['srcshape'] = cls['srcshape'].string()
798   - phagen['coordsys'] = cls['coordsys'].string()
799   - if cls['coordsys'].string() == 'CEL':
800   - phagen['ra'] = cls['xref'].real()
801   - phagen['dec'] = cls['yref'].real()
802   - elif cls['coordsys'].string() == 'GAL':
803   - phagen['glon'] = cls['xref'].real()
804   - phagen['glat'] = cls['yref'].real()
  803 + # User has specified custom centre for the source region
  804 + if ra != None and dec != None:
  805 + phagen['coordsys'] = 'CEL'
  806 + phagen['ra'] = ra
  807 + phagen['dec'] = dec
  808 + # Otherwise use default in class
  809 + else:
  810 + phagen['coordsys'] = cls['coordsys'].string()
  811 + if cls['coordsys'].string() == 'CEL':
  812 + phagen['ra'] = cls['xref'].real()
  813 + phagen['dec'] = cls['yref'].real()
  814 + elif cls['coordsys'].string() == 'GAL':
  815 + phagen['glon'] = cls['xref'].real()
  816 + phagen['glat'] = cls['yref'].real()
805 817 if cls['srcshape'].string() == 'CIRCLE':
806 818 phagen['rad'] = cls['rad'].real()
807 819 phagen['bkgmethod'] = cls['bkgmethod'].string()
... ...