Commit 57ce6beb85279b6b95735246f15e4b5edc910fed

Authored by Jürgen Knödlseder
2 parents 59d3205f 1898bd89

Merge branch '3436-off-runs' into integration

cscripts/Makefile.am
@@ -182,6 +182,7 @@ testdata = $(top_srcdir)/test/data/crab.xml \ @@ -182,6 +182,7 @@ testdata = $(top_srcdir)/test/data/crab.xml \
182 $(top_srcdir)/test/data/crab_offaxis1.fits.gz \ 182 $(top_srcdir)/test/data/crab_offaxis1.fits.gz \
183 $(top_srcdir)/test/data/crab_offaxis2.fits.gz \ 183 $(top_srcdir)/test/data/crab_offaxis2.fits.gz \
184 $(top_srcdir)/test/data/crab_exclusion.fits.gz \ 184 $(top_srcdir)/test/data/crab_exclusion.fits.gz \
  185 + $(top_srcdir)/test/data/background_events.fits.gz \
185 $(top_srcdir)/test/data/crab_src_reg.reg \ 186 $(top_srcdir)/test/data/crab_src_reg.reg \
186 $(top_srcdir)/test/data/crab_bkg_reg.reg \ 187 $(top_srcdir)/test/data/crab_bkg_reg.reg \
187 $(top_srcdir)/test/data/tsmap_0.fits \ 188 $(top_srcdir)/test/data/tsmap_0.fits \
cscripts/csphagen.par
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 # Input and output parameters 17 # Input and output parameters
18 #============================ 18 #============================
19 inobs, f, a, obs.xml,,, "Input event list or observation definition XML file" 19 inobs, f, a, obs.xml,,, "Input event list or observation definition XML file"
  20 +inobsoff, f, a, obs_off.xml,,, "Input Off event list or observation definition XML file"
20 inmodel, f, a, NONE,,, "Input model definition XML file (if NONE, use point source)" 21 inmodel, f, a, NONE,,, "Input model definition XML file (if NONE, use point source)"
21 srcname, s, a, Crab,,, "Source name" 22 srcname, s, a, Crab,,, "Source name"
22 caldb, s, a, prod2,,, "Calibration database" 23 caldb, s, a, prod2,,, "Calibration database"
@@ -46,10 +47,10 @@ width, r, a, 0.2,0,180, "Width of source region rectangle (deg)" @@ -46,10 +47,10 @@ width, r, a, 0.2,0,180, "Width of source region rectangle (deg)"
46 height, r, a, 0.2,0,180, "Height of source region rectangle (deg)" 47 height, r, a, 0.2,0,180, "Height of source region rectangle (deg)"
47 posang, r, a, 0.0,-360,360, "Position angle of source region rectangle (deg)" 48 posang, r, a, 0.0,-360,360, "Position angle of source region rectangle (deg)"
48 srcregfile, f, a, on.reg,,, "Source region file (ds9 or FITS WCS)" 49 srcregfile, f, a, on.reg,,, "Source region file (ds9 or FITS WCS)"
49 -bkgmethod, s, a, REFLECTED,REFLECTED|CUSTOM,, "Method for background estimation" 50 +bkgmethod, s, a, REFLECTED,REFLECTED|CUSTOM|OFF,, "Method for background estimation"
50 bkgregfile, f, a, bkg.reg,,, "Background regions file (ds9 or FITS WCS)" 51 bkgregfile, f, a, bkg.reg,,, "Background regions file (ds9 or FITS WCS)"
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 +bkgregmin, i, h, 2,1,1000, "Minimum number of background regions for REFLECTED method"
  53 +bkgregskip, i, h, 1,0,10, "Number of background regions to skip next to On region for REFLECTED method"
53 use_model_bkg, b, h, yes,,, "Use background model?" 54 use_model_bkg, b, h, yes,,, "Use background model?"
54 maxoffset, r, h, 4.0,,, "Maximum offset of source from camera center to accept observation" 55 maxoffset, r, h, 4.0,,, "Maximum offset of source from camera center to accept observation"
55 stack, b, a, no,,, "Stack multiple observations into single PHA, ARF and RMF files?" 56 stack, b, a, no,,, "Stack multiple observations into single PHA, ARF and RMF files?"
cscripts/csphagen.py
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 # Computes the PHA spectra for source/background and ARF/RMF files using the 3 # Computes the PHA spectra for source/background and ARF/RMF files using the
4 # reflected region method 4 # reflected region method
5 # 5 #
6 -# Copyright (C) 2017-2020 Luigi Tibaldo 6 +# Copyright (C) 2017-2021 Luigi Tibaldo
7 # 7 #
8 # This program is free software: you can redistribute it and/or modify 8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by 9 # it under the terms of the GNU General Public License as published by
@@ -43,6 +43,7 @@ class csphagen(ctools.csobservation): @@ -43,6 +43,7 @@ class csphagen(ctools.csobservation):
43 self._init_csobservation(self.__class__.__name__, ctools.__version__, argv) 43 self._init_csobservation(self.__class__.__name__, ctools.__version__, argv)
44 44
45 # Initialise other variables 45 # Initialise other variables
  46 + self._obs_off = gammalib.GObservations()
46 self._ebounds = gammalib.GEbounds() 47 self._ebounds = gammalib.GEbounds()
47 self._etruebounds = gammalib.GEbounds() 48 self._etruebounds = gammalib.GEbounds()
48 self._src_dir = gammalib.GSkyDir() 49 self._src_dir = gammalib.GSkyDir()
@@ -74,6 +75,7 @@ class csphagen(ctools.csobservation): @@ -74,6 +75,7 @@ class csphagen(ctools.csobservation):
74 """ 75 """
75 # Set pickled dictionary 76 # Set pickled dictionary
76 state = {'base' : ctools.csobservation.__getstate__(self), 77 state = {'base' : ctools.csobservation.__getstate__(self),
  78 + 'obs_off' : self._obs_off,
77 'ebounds' : self._ebounds, 79 'ebounds' : self._ebounds,
78 'etruebounds' : self._etruebounds, 80 'etruebounds' : self._etruebounds,
79 'src_dir' : self._src_dir, 81 'src_dir' : self._src_dir,
@@ -103,6 +105,7 @@ class csphagen(ctools.csobservation): @@ -103,6 +105,7 @@ class csphagen(ctools.csobservation):
103 Pickled instance 105 Pickled instance
104 """ 106 """
105 ctools.csobservation.__setstate__(self, state['base']) 107 ctools.csobservation.__setstate__(self, state['base'])
  108 + self._obs_off = state['obs_off']
106 self._ebounds = state['ebounds'] 109 self._ebounds = state['ebounds']
107 self._etruebounds = state['etruebounds'] 110 self._etruebounds = state['etruebounds']
108 self._src_dir = state['src_dir'] 111 self._src_dir = state['src_dir']
@@ -205,21 +208,17 @@ class csphagen(ctools.csobservation): @@ -205,21 +208,17 @@ class csphagen(ctools.csobservation):
205 # Return region container 208 # Return region container
206 return regs 209 return regs
207 210
208 - def _get_parameters_bkgmethod_reflected(self): 211 + def _get_source_parameters(self):
209 """ 212 """
210 - Get parameters for REFLECTED background method 213 + Get parameters to define source/On region
211 """ 214 """
  215 +
212 # Get source shape 216 # Get source shape
213 self._srcshape = self['srcshape'].string() 217 self._srcshape = self['srcshape'].string()
214 218
215 # Query source direction 219 # Query source direction
216 self._query_src_direction() 220 self._query_src_direction()
217 221
218 - # Query minimum number of background regions and number of  
219 - # background regions to skip next to On region  
220 - self['bkgregmin'].integer()  
221 - self['bkgregskip'].integer()  
222 -  
223 # If source shape is a circle the append GSkyRegionCircle 222 # If source shape is a circle the append GSkyRegionCircle
224 if self._srcshape == 'CIRCLE': 223 if self._srcshape == 'CIRCLE':
225 224
@@ -240,8 +239,21 @@ class csphagen(ctools.csobservation): @@ -240,8 +239,21 @@ class csphagen(ctools.csobservation):
240 self._reg_height, 239 self._reg_height,
241 self._reg_posang)) 240 self._reg_posang))
242 241
243 - # Query usage of background model  
244 - self['use_model_bkg'].boolean() 242 + # Return
  243 + return
  244 +
  245 + def _get_parameters_bkgmethod_reflected(self):
  246 + """
  247 + Get parameters for REFLECTED background method
  248 + """
  249 +
  250 + # Query parameters for source/On region definition
  251 + self._get_source_parameters()
  252 +
  253 + # Query minimum number of background regions and
  254 + # number of background regions to skip next to On region
  255 + self['bkgregmin'].integer()
  256 + self['bkgregskip'].integer()
245 257
246 # Return 258 # Return
247 return 259 return
@@ -284,6 +296,76 @@ class csphagen(ctools.csobservation): @@ -284,6 +296,76 @@ class csphagen(ctools.csobservation):
284 # Return 296 # Return
285 return 297 return
286 298
  299 + def _get_parameters_bkgmethod_off(self):
  300 + """
  301 + Get parameters for OFF background method
  302 +
  303 + Raises
  304 + ------
  305 + RuntimeError
  306 + On and Off observations must have same size
  307 + RuntimeError
  308 + Off observations must be event lists
  309 + """
  310 +
  311 + # Set up Off observations. If there are no Off observations in the
  312 + # container then load them via user parameters
  313 + if self.obs_off().is_empty():
  314 +
  315 + # Get Off observation file name
  316 + filename = self['inobsoff'].filename()
  317 +
  318 + # If Off observation is a FITS file then load observation and
  319 + # append it to the Off observation container
  320 + if gammalib.GFilename(filename).is_fits():
  321 + self._obs_off.append(gammalib.GCTAObservation(filename))
  322 +
  323 + # ... otherwise load XML file into Off observation container
  324 + else:
  325 + self._obs_off.load(filename)
  326 +
  327 + # Check that size of On and Off observations are the same, otherwise
  328 + # raise error
  329 + if self.obs().size() != self.obs_off().size():
  330 + raise RuntimeError('On and Off observations must have the same size')
  331 +
  332 + # Loop through observations
  333 + for obs in self.obs_off():
  334 +
  335 + # Check that observation is event list, otherwise throw error
  336 + if obs.eventtype() != "EventList":
  337 + raise RuntimeError('Off observations must be event lists')
  338 +
  339 + # Check that they have response, otherwise assign based on user parameter
  340 + if obs.has_response() == False:
  341 +
  342 + # Get database and IRF
  343 + database = self["caldb"].string()
  344 + irf = self["irf"].string()
  345 +
  346 + # Create an XML element for response
  347 + parameter = "parameter name=\"Calibration\"" +\
  348 + " database=\"" + database + "\"" +\
  349 + " response=\"" + irf + "\""
  350 + xml = gammalib.GXmlElement()
  351 + xml.append(parameter)
  352 +
  353 + # Create CTA response
  354 + response = gammalib.GCTAResponseIrf(xml)
  355 +
  356 + # Attach response to observation
  357 + obs.response(response)
  358 +
  359 + # Add models from Off observations to model container
  360 + for model in self.obs_off().models():
  361 + self._models.append(model)
  362 +
  363 + # Query parameters for source/On region definition
  364 + self._get_source_parameters()
  365 +
  366 + # Return
  367 + return
  368 +
287 def _get_parameters_bkgmethod(self): 369 def _get_parameters_bkgmethod(self):
288 """ 370 """
289 Get background method parameters 371 Get background method parameters
@@ -296,9 +378,12 @@ class csphagen(ctools.csobservation): @@ -296,9 +378,12 @@ class csphagen(ctools.csobservation):
296 self._get_parameters_bkgmethod_reflected() 378 self._get_parameters_bkgmethod_reflected()
297 elif bkgmethod == 'CUSTOM': 379 elif bkgmethod == 'CUSTOM':
298 self._get_parameters_bkgmethod_custom() 380 self._get_parameters_bkgmethod_custom()
  381 + elif bkgmethod == 'OFF':
  382 + self._get_parameters_bkgmethod_off()
299 383
300 # Query parameters that are needed for all background methods 384 # Query parameters that are needed for all background methods
301 self['maxoffset'].real() 385 self['maxoffset'].real()
  386 + self['use_model_bkg'].boolean()
302 387
303 # Return 388 # Return
304 return 389 return
@@ -351,9 +436,13 @@ class csphagen(ctools.csobservation): @@ -351,9 +436,13 @@ class csphagen(ctools.csobservation):
351 else: 436 else:
352 self._has_exclusion = False 437 self._has_exclusion = False
353 438
354 - # Query remaining parameters  
355 - self['use_model_bkg'].boolean()  
356 - self['stack'].boolean() 439 + # Get background method parameters (have to come after setting up of
  440 + # observations and models)
  441 + self._get_parameters_bkgmethod()
  442 +
  443 + # If there are multiple observations query whether to stack them
  444 + if self.obs().size() > 1:
  445 + self['stack'].boolean()
357 446
358 # Query ahead output parameters 447 # Query ahead output parameters
359 if (self._read_ahead()): 448 if (self._read_ahead()):
@@ -361,15 +450,6 @@ class csphagen(ctools.csobservation): @@ -361,15 +450,6 @@ class csphagen(ctools.csobservation):
361 self['outmodel'].filename() 450 self['outmodel'].filename()
362 self['prefix'].string() 451 self['prefix'].string()
363 452
364 - # If there are no observations in container then get them from the  
365 - # parameter file  
366 - if self.obs().is_empty():  
367 - self.obs(self._get_observations(False))  
368 -  
369 - # Get background method parameters (have to come after setting up of  
370 - # observations)  
371 - self._get_parameters_bkgmethod()  
372 -  
373 # Write input parameters into logger 453 # Write input parameters into logger
374 self._log_parameters(gammalib.TERSE) 454 self._log_parameters(gammalib.TERSE)
375 455
@@ -459,14 +539,6 @@ class csphagen(ctools.csobservation): @@ -459,14 +539,6 @@ class csphagen(ctools.csobservation):
459 if self._srcshape == 'CIRCLE': 539 if self._srcshape == 'CIRCLE':
460 msg += ' (circle rad=%.3f).' % (self._rad) 540 msg += ' (circle rad=%.3f).' % (self._rad)
461 self._log_string(gammalib.NORMAL, msg) 541 self._log_string(gammalib.NORMAL, msg)
462 -  
463 - # Skip observation if it is pointed too far from the source  
464 - elif offset >= self['maxoffset'].real():  
465 - msg = ' Skip because observation is pointed at %.3f deg >= '\  
466 - '"maxoffset=%.3f" from source.' \  
467 - % (offset, self['maxoffset'].real())  
468 - self._log_string(gammalib.NORMAL, msg)  
469 -  
470 # ... otherwise 542 # ... otherwise
471 else: 543 else:
472 posang = pnt_dir.posang_deg(self._src_dir) 544 posang = pnt_dir.posang_deg(self._src_dir)
@@ -474,7 +546,6 @@ class csphagen(ctools.csobservation): @@ -474,7 +546,6 @@ class csphagen(ctools.csobservation):
474 546
475 # Determine number of background regions to skip 547 # Determine number of background regions to skip
476 N_skip = self['bkgregskip'].integer() 548 N_skip = self['bkgregskip'].integer()
477 - N_start = 1 + N_skip  
478 N_lim = 1 + 2 * N_skip 549 N_lim = 1 + 2 * N_skip
479 550
480 # Compute the angular separation of reflected regions wrt 551 # Compute the angular separation of reflected regions wrt
@@ -532,13 +603,82 @@ class csphagen(ctools.csobservation): @@ -532,13 +603,82 @@ class csphagen(ctools.csobservation):
532 regions.append(region) 603 regions.append(region)
533 dphi += alpha 604 dphi += alpha
534 605
535 - # Log number of reflected regions  
536 - msg = ' Use %d reflected regions.' % (regions.size())  
537 - self._log_string(gammalib.NORMAL, msg) 606 + # Check again that we have enough background regions
  607 + # now that we have checked for overlap with exclusion region
  608 + if regions.size() >= self['bkgregmin'].integer():
  609 + # Log number of reflected regions
  610 + msg = ' Use %d reflected regions.' % (regions.size())
  611 + self._log_string(gammalib.NORMAL, msg)
  612 + # Otherwise log observation skipped and return empty region container
  613 + else:
  614 + msg = ' Skip because the number %d of regions' \
  615 + 'for background estimation not overlapping ' \
  616 + 'with the exclusion region is smaller than ' \
  617 + '"bkgregmin"=%d.' % (bkg_reg.size(), self['bkgregmin'].integer())
  618 + self._log_string(gammalib.NORMAL, msg)
  619 + regions = gammalib.GSkyRegions()
538 620
539 # Return reflected regions 621 # Return reflected regions
540 return regions 622 return regions
541 623
  624 + def _instrument_regions(self, obs, obs_off):
  625 + """
  626 + Compute background regions for Off observation
  627 +
  628 + Calculate background region in Off observation that corresponds to the
  629 + source region in the On observation in instrument coordinates
  630 +
  631 + Parameters
  632 + ----------
  633 + obs : `~gammalib.GCTAObservation()`
  634 + On CTA observation
  635 + obs_off : `~gammalib.GCTAObservation()`
  636 + Off CTA observation
  637 +
  638 + Returns
  639 + -------
  640 + regions : `~gammalib.GSkyRegions`
  641 + Container with background region
  642 + """
  643 + # Initialise region container
  644 + regions = gammalib.GSkyRegions()
  645 +
  646 + # Convert source position in On observation to instrument coordinates
  647 + instdir = obs.pointing().instdir(self._src_dir)
  648 +
  649 + # Convert instrument position to sky direction for Off observation
  650 + off_dir = obs_off.pointing().skydir(instdir)
  651 +
  652 + # Build region according to shape specified by user
  653 + # If circle
  654 + if self._srcshape == 'CIRCLE':
  655 + region = gammalib.GSkyRegionCircle(off_dir, self._rad)
  656 +
  657 + # ... otherwise if rectangle
  658 + elif self._srcshape == 'RECT':
  659 + # Instrument coordinates take sky direction as reference
  660 + # so no need to change the position angle
  661 + region = gammalib.GSkyRegionRectangle(off_dir,
  662 + self._reg_width,
  663 + self._reg_height,
  664 + self._reg_posang)
  665 +
  666 + # Check if background region overlaps with exclusion region
  667 + is_valid = True
  668 + if self._has_exclusion:
  669 + if self._excl_reg.overlaps(region):
  670 + # Signal region overlap
  671 + msg = ' Background region overlaps with exclusion region.'
  672 + self._log_string(gammalib.EXPLICIT, msg)
  673 + is_valid = False
  674 +
  675 + # If region is valid then append it to container
  676 + if is_valid:
  677 + regions.append(region)
  678 +
  679 + # Return
  680 + return regions
  681 +
542 def _set_models(self, results): 682 def _set_models(self, results):
543 """ 683 """
544 Set models for On/Off fitting 684 Set models for On/Off fitting
@@ -587,7 +727,7 @@ class csphagen(ctools.csobservation): @@ -587,7 +727,7 @@ class csphagen(ctools.csobservation):
587 # observations 727 # observations
588 for result in results: 728 for result in results:
589 if model.is_valid(result['instrument'], result['id']): 729 if model.is_valid(result['instrument'], result['id']):
590 - if result['bkg_reg'].size() >= self['bkgregmin'].integer(): 730 + if result['bkg_reg'].size() > 0:
591 use_model = True 731 use_model = True
592 break 732 break
593 733
@@ -699,7 +839,7 @@ class csphagen(ctools.csobservation): @@ -699,7 +839,7 @@ class csphagen(ctools.csobservation):
699 # Return energy boundaries 839 # Return energy boundaries
700 return ebounds 840 return ebounds
701 841
702 - def _set_background_regions(self, obs): 842 + def _set_background_regions(self, obs, obs_off=None):
703 """ 843 """
704 Set background regions for an observation 844 Set background regions for an observation
705 845
@@ -716,7 +856,7 @@ class csphagen(ctools.csobservation): @@ -716,7 +856,7 @@ class csphagen(ctools.csobservation):
716 # Initialise empty background regions for this observation 856 # Initialise empty background regions for this observation
717 bkg_reg = gammalib.GSkyRegions() 857 bkg_reg = gammalib.GSkyRegions()
718 858
719 - # If reflected background is requested then created reflected 859 + # If reflected background is requested then create reflected
720 # background regions 860 # background regions
721 if self['bkgmethod'].string() == 'REFLECTED': 861 if self['bkgmethod'].string() == 'REFLECTED':
722 bkg_reg = self._reflected_regions(obs) 862 bkg_reg = self._reflected_regions(obs)
@@ -728,6 +868,11 @@ class csphagen(ctools.csobservation): @@ -728,6 +868,11 @@ class csphagen(ctools.csobservation):
728 elif self['bkgmethod'].string() == 'CUSTOM': 868 elif self['bkgmethod'].string() == 'CUSTOM':
729 bkg_reg = obs.off_regions().copy() 869 bkg_reg = obs.off_regions().copy()
730 870
  871 + # ... otherwise if dedicated Off runs are use then
  872 + # use background region that correspond to the same instrument coordinates
  873 + if self['bkgmethod'].string() == 'OFF':
  874 + bkg_reg = self._instrument_regions(obs,obs_off)
  875 +
731 # Return background regions 876 # Return background regions
732 return bkg_reg 877 return bkg_reg
733 878
@@ -750,6 +895,13 @@ class csphagen(ctools.csobservation): @@ -750,6 +895,13 @@ class csphagen(ctools.csobservation):
750 bkg_reg = None 895 bkg_reg = None
751 obs = self.obs()[i] 896 obs = self.obs()[i]
752 897
  898 + # Retrieve dedicated Off observation if it exists
  899 + if not self.obs_off().is_empty():
  900 + obs_off = self.obs_off()[i]
  901 + # Otherwise use the same as On
  902 + else:
  903 + obs_off = self.obs()[i]
  904 +
753 # Log header 905 # Log header
754 self._log_header3(gammalib.NORMAL,'%s observation "%s"' % \ 906 self._log_header3(gammalib.NORMAL,'%s observation "%s"' % \
755 (obs.instrument(), obs.id())) 907 (obs.instrument(), obs.id()))
@@ -768,32 +920,43 @@ class csphagen(ctools.csobservation): @@ -768,32 +920,43 @@ class csphagen(ctools.csobservation):
768 msg = ' Background model not used, assume constant backround rate.' 920 msg = ' Background model not used, assume constant backround rate.'
769 self._log_string(gammalib.NORMAL, msg) 921 self._log_string(gammalib.NORMAL, msg)
770 922
771 - # Set background regions for this observation  
772 - bkg_reg = self._set_background_regions(obs) 923 + # Get offset angle of source
  924 + pnt_dir = obs.pointing().dir()
  925 + offset = pnt_dir.dist_deg(self._src_dir)
773 926
774 - # If there are background regions then create On/Off observation  
775 - # and append it to the output container  
776 - if bkg_reg.size() >= self['bkgregmin'].integer(): 927 + # Skip observation if it is pointed too far from the source
  928 + if offset >= self['maxoffset'].real():
  929 + msg = ' Skip because observation is pointed at %.3f deg >= ' \
  930 + '"maxoffset=%.3f" from source.' \
  931 + % (offset, self['maxoffset'].real())
  932 + self._log_string(gammalib.NORMAL, msg)
  933 + # ... otherwise continue to process
  934 + else:
777 935
778 - # Create On/Off observation  
779 - onoff = gammalib.GCTAOnOffObservation(obs,  
780 - self._models,  
781 - self._srcname,  
782 - self._etruebounds,  
783 - self._ebounds,  
784 - self._src_reg,  
785 - bkg_reg,  
786 - use_model_bkg) 936 + # Set background regions for this observation
  937 + bkg_reg = self._set_background_regions(obs,obs_off)
787 938
788 - # Set On/Off observation ID  
789 - onoff.id(obs.id()) 939 + # If there are any background regions then create On/Off observation
  940 + # and append it to the output container
  941 + if bkg_reg.size() >= 0:
790 942
791 - # Otherwise log observation skipped  
792 - else:  
793 - msg = ' Skip because the number %d of regions ' \  
794 - 'for background estimation is smaller than ' \  
795 - '"bkgregmin"=%d.' % (bkg_reg.size(), self['bkgregmin'].integer())  
796 - self._log_string(gammalib.NORMAL, msg) 943 + # Create On/Off observation
  944 + onoff = gammalib.GCTAOnOffObservation(obs, obs_off,
  945 + self._models,
  946 + self._srcname,
  947 + self._etruebounds,
  948 + self._ebounds,
  949 + self._src_reg,
  950 + bkg_reg,
  951 + use_model_bkg)
  952 +
  953 + # Set On/Off observation ID
  954 + onoff.id(obs.id())
  955 +
  956 + # Otherwise log observation skipped
  957 + else:
  958 + msg = ' Skip because no valid Off regions could be determined'
  959 + self._log_string(gammalib.NORMAL, msg)
797 960
798 # Construct dictionary with results 961 # Construct dictionary with results
799 result = {'onoff' : onoff, 962 result = {'onoff' : onoff,
@@ -854,6 +1017,8 @@ class csphagen(ctools.csobservation): @@ -854,6 +1017,8 @@ class csphagen(ctools.csobservation):
854 1017
855 # Write observation into logger 1018 # Write observation into logger
856 self._log_observations(gammalib.NORMAL, self.obs(), 'Observation') 1019 self._log_observations(gammalib.NORMAL, self.obs(), 'Observation')
  1020 + if not self.obs_off().is_empty():
  1021 + self._log_observations(gammalib.NORMAL, self._obs_off, 'Off Observation')
857 1022
858 # Set true energy bins 1023 # Set true energy bins
859 self._etruebounds = self._etrue_ebounds() 1024 self._etruebounds = self._etrue_ebounds()
@@ -1032,6 +1197,27 @@ class csphagen(ctools.csobservation): @@ -1032,6 +1197,27 @@ class csphagen(ctools.csobservation):
1032 # Return 1197 # Return
1033 return self._excl_reg 1198 return self._excl_reg
1034 1199
  1200 + def obs_off(self, obs=None):
  1201 + """
  1202 + Return and optionally set the Off observations
  1203 +
  1204 + Parameters
  1205 + ----------
  1206 + obs : `~gammalib.GCTAObservations`
  1207 + Off observations container
  1208 +
  1209 + Returns
  1210 + -------
  1211 + observation container : `~gammalib.GCTAObservations`
  1212 + Off observations container
  1213 + """
  1214 + # If an observation container is provided then set the Off observations ...
  1215 + if obs is not None:
  1216 + self._obs_off = obs
  1217 +
  1218 + # Return
  1219 + return self._obs_off
  1220 +
1035 1221
1036 # ======================== # 1222 # ======================== #
1037 # Main routine entry point # 1223 # Main routine entry point #
doc/source/users/reference_manual/csphagen.rst
@@ -17,17 +17,16 @@ the OGIP format normally used in X-ray astronomy (PHA, ARF, RMF), @@ -17,17 +17,16 @@ the OGIP format normally used in X-ray astronomy (PHA, ARF, RMF),
17 If the ``bkgmethod`` parameter is ``REFLECTED``, the script will use an On region 17 If the ``bkgmethod`` parameter is ``REFLECTED``, the script will use an On region
18 shape defined by the ``srcshape`` parameter and generate reflected regions by 18 shape defined by the ``srcshape`` parameter and generate reflected regions by
19 placing regions of the same shape that are reflected with respect to the 19 placing regions of the same shape that are reflected with respect to the
20 -pointing direction. So far circular and rectangular regions are supported.  
21 -Circular regions are defined by a center that is specified by the ``ra`` and  
22 -``dec`` parameters if ``coordsys=CEL`` or the ``glon`` and ``glat`` parameters if 20 +pointing direction within the same observation. So far circular and
  21 +rectangular regions are supported. Circular regions are defined by a
  22 +center that is specified by the ``ra`` and ``dec`` parameters if
  23 +``coordsys=CEL`` or the ``glon`` and ``glat`` parameters if
23 ``coordsys=GAL``, and a radius that is specified by the ``rad`` parameter. 24 ``coordsys=GAL``, and a radius that is specified by the ``rad`` parameter.
24 Rectangular regions are defined by a center, again specified by either the 25 Rectangular regions are defined by a center, again specified by either the
25 ``ra`` and ``dec`` or the ``glon`` and ``glat`` parameters, a ``width`` parameter, 26 ``ra`` and ``dec`` or the ``glon`` and ``glat`` parameters, a ``width`` parameter,
26 a ``height`` parameter and a ``posang`` parameter that specified the position 27 a ``height`` parameter and a ``posang`` parameter that specified the position
27 angle measured counter clockwise from celestial North. Note that the ``height`` 28 angle measured counter clockwise from celestial North. Note that the ``height``
28 -axis is pointing in the direction of the position angle. Reflected regions are  
29 -only generated for offset angles with respect to the pointing direction that are  
30 -smaller than the value specified by the ``maxoffset`` parameter. If less than 29 +axis is pointing in the direction of the position angle. If less than
31 ``bkgregmin`` reflected regions can be found because the offset angle between 30 ``bkgregmin`` reflected regions can be found because the offset angle between
32 source and pointing direction is too small, the observation will be skipped. 31 source and pointing direction is too small, the observation will be skipped.
33 Regions to be excluded for background determination can be specified using a 32 Regions to be excluded for background determination can be specified using a
@@ -44,7 +43,7 @@ respectively. Both parameters accept either a ds9 region file or a sky map where @@ -44,7 +43,7 @@ respectively. Both parameters accept either a ds9 region file or a sky map where
44 all non-zero pixel values define a region. In case that an observation definition 43 all non-zero pixel values define a region. In case that an observation definition
45 XML file is specified as input using the ``inobs``, background region files can 44 XML file is specified as input using the ``inobs``, background region files can
46 be specified separately for each observation using the ``OffRegions`` parameter 45 be specified separately for each observation using the ``OffRegions`` parameter
47 -in the format 46 +in the format.
48 47
49 .. code-block:: xml 48 .. code-block:: xml
50 49
@@ -53,6 +52,16 @@ in the format @@ -53,6 +52,16 @@ in the format
53 ... 52 ...
54 </observation> 53 </observation>
55 54
  55 +Finally, if ``bkgmethod=OFF`` is specified, the background regions
  56 +will be taken from independent Off observations given by the
  57 +``inobsoff`` parameter. The On region can be defined in the same way
  58 +as for ``bkgmethod=REFLECTED``, and the Off regions are the
  59 +corresponding regions in instrument coordinates from the Off observations.
  60 +
  61 +For all background estimation methods only observations with a
  62 +pointing direction at an offset smaller than ``maxoffset`` from the
  63 +source are used.
  64 +
56 :ref:`csphagen` supports multiprocessing. By default each event list will be treated 65 :ref:`csphagen` supports multiprocessing. By default each event list will be treated
57 in parallel over as many processes as the number of CPUs available on your 66 in parallel over as many processes as the number of CPUs available on your
58 machine. The maximum number of parallel processes can be set by the user through the 67 machine. The maximum number of parallel processes can be set by the user through the
@@ -78,6 +87,11 @@ General parameters @@ -78,6 +87,11 @@ General parameters
78 ``inobs [file]`` 87 ``inobs [file]``
79 Input event list or observation definition XML file. 88 Input event list or observation definition XML file.
80 89
  90 +``inobsoff [file]``
  91 + Input event list or observation definition XML file for dedicated
  92 + Off observations. The number of observations must be the same as
  93 + for ``inobs``.
  94 +
81 ``inmodel [file]`` 95 ``inmodel [file]``
82 Input model XML file (if ``NONE`` a point source at the centre of the 96 Input model XML file (if ``NONE`` a point source at the centre of the
83 source region is used). 97 source region is used).
@@ -172,7 +186,7 @@ General parameters @@ -172,7 +186,7 @@ General parameters
172 ``srcregfile [file]`` 186 ``srcregfile [file]``
173 Source region file (ds9 or FITS WCS map). 187 Source region file (ds9 or FITS WCS map).
174 188
175 -``bkgmethod <REFLECTED|CUSTOM> [string]`` 189 +``bkgmethod <REFLECTED|CUSTOM|OFF> [string]``
176 Method for background estimation: 190 Method for background estimation:
177 191
178 - ``REFLECTED``: background is evaluated in regions with the same shape as 192 - ``REFLECTED``: background is evaluated in regions with the same shape as
@@ -184,18 +198,25 @@ General parameters @@ -184,18 +198,25 @@ General parameters
184 observations specified in the observation definition XML file the name of 198 observations specified in the observation definition XML file the name of
185 the region file will be extracted from the ``OffRegions`` parameter that 199 the region file will be extracted from the ``OffRegions`` parameter that
186 needs to be specified for each observation in the observation definition 200 needs to be specified for each observation in the observation definition
187 - XML file. Off region files can be either ds9 region files or FITS WCS maps. 201 + XML file. Off region files can be either ds9 region files or
  202 + FITS WCS maps.
  203 +
  204 + - ``OFF``: background is evaluated from dedicated Off
  205 + observations in regions that correspond to the source region
  206 + in the On observations in instrument coordinates.
188 207
189 ``bkgregfile [file]`` 208 ``bkgregfile [file]``
190 Background regions file (ds9 or FITS WCS map). 209 Background regions file (ds9 or FITS WCS map).
191 210
192 ``(bkgregmin = 2) [integer]`` 211 ``(bkgregmin = 2) [integer]``
193 - Minimum number of background regions that are required for an observation. 212 + Minimum number of background regions that are required for an
  213 + observation when the ``REFLECTED`` background estimation is used.
194 If this number of background regions is not available the observation is 214 If this number of background regions is not available the observation is
195 skipped. 215 skipped.
196 216
197 ``(bkgregskip = 1) [integer]`` 217 ``(bkgregskip = 1) [integer]``
198 - Number of background regions that should be skipped next to the On regions. 218 + Number of background regions that should be skipped next to the On
  219 + region when the ``REFLECTED`` background estimation is used.
199 Typically, one region is skipped so that the Off regions are taken sufficiently 220 Typically, one region is skipped so that the Off regions are taken sufficiently
200 distant from the On region, but in some cases it may be useful to keep the 221 distant from the On region, but in some cases it may be useful to keep the
201 background regions next to the On region. 222 background regions next to the On region.
test/data/background_events.fits.gz 0 → 100644
No preview for this file type
test/test_csphagen.py
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 # ========================================================================== 2 # ==========================================================================
3 # This scripts performs unit tests for the csphagen script. 3 # This scripts performs unit tests for the csphagen script.
4 # 4 #
5 -# Copyright (C) 2017-2020 Luigi Tibaldo 5 +# Copyright (C) 2017-2021 Luigi Tibaldo
6 # 6 #
7 # This program is free software: you can redistribute it and/or modify 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 8 # it under the terms of the GNU General Public License as published by
@@ -46,6 +46,7 @@ class Test(test): @@ -46,6 +46,7 @@ class Test(test):
46 self._myevents1 = self._datadir + '/crab_offaxis1.fits' 46 self._myevents1 = self._datadir + '/crab_offaxis1.fits'
47 self._myevents2 = self._datadir + '/crab_offaxis2.fits' 47 self._myevents2 = self._datadir + '/crab_offaxis2.fits'
48 self._exclusion = self._datadir + '/crab_exclusion.fits' 48 self._exclusion = self._datadir + '/crab_exclusion.fits'
  49 + self._offevents = self._datadir + '/background_events.fits'
49 self._nreg_with_excl = 5 50 self._nreg_with_excl = 5
50 self._nreg_wo_excl = 8 51 self._nreg_wo_excl = 8
51 self._nreg_bkg_reg = 5 52 self._nreg_bkg_reg = 5
@@ -414,6 +415,117 @@ class Test(test): @@ -414,6 +415,117 @@ class Test(test):
414 self._check_output('csphagen_py8', self._nbins, 9) 415 self._check_output('csphagen_py8', self._nbins, 9)
415 self._check_outobs('csphagen_py8_obs.xml', 1) 416 self._check_outobs('csphagen_py8_obs.xml', 1)
416 417
  418 + # Setup csphagen for test with dedicated Off observations
  419 + phagen = cscripts.csphagen()
  420 + phagen['inobs'] = self._myevents1
  421 + phagen['inobsoff'] = self._offevents
  422 + phagen['inmodel'] = 'NONE'
  423 + phagen['caldb'] = self._caldb
  424 + phagen['irf'] = self._irf
  425 + phagen['srcshape'] = 'CIRCLE'
  426 + phagen['coordsys'] = 'CEL'
  427 + phagen['ra'] = 83.633
  428 + phagen['dec'] = 22.0145
  429 + phagen['rad'] = 0.2
  430 + phagen['ebinalg'] = 'LOG'
  431 + phagen['emin'] = 0.1
  432 + phagen['emax'] = 100.0
  433 + phagen['enumbins'] = self._nbins
  434 + phagen['bkgmethod'] = 'OFF'
  435 + phagen['etruemin'] = 0.05
  436 + phagen['etruemax'] = 150.0
  437 + phagen['etruebins'] = 5
  438 + phagen['stack'] = False
  439 + phagen['outobs'] = 'csphagen_py9_obs.xml'
  440 + phagen['outmodel'] = 'csphagen_py9_model.xml'
  441 + phagen['prefix'] = 'csphagen_py9'
  442 + phagen['logfile'] = 'csphagen_py9.log'
  443 + phagen['chatter'] = 2
  444 +
  445 + # Execute script
  446 + phagen.execute()
  447 +
  448 + # Check output
  449 + self._check_output('csphagen_py9', self._nbins, 1)
  450 + self._check_outobs('csphagen_py9_obs.xml', 1)
  451 +
  452 + # Setup csphagen for test with dedicated Off observations and
  453 + # exclusion region
  454 + phagen = cscripts.csphagen()
  455 + phagen['inobs'] = self._myevents1
  456 + phagen['inobsoff'] = self._offevents
  457 + phagen['inmodel'] = 'NONE'
  458 + phagen['caldb'] = self._caldb
  459 + phagen['irf'] = self._irf
  460 + phagen['srcshape'] = 'CIRCLE'
  461 + phagen['coordsys'] = 'CEL'
  462 + phagen['ra'] = 83.633
  463 + phagen['dec'] = 22.0145
  464 + phagen['rad'] = 0.2
  465 + phagen['ebinalg'] = 'LOG'
  466 + phagen['emin'] = 0.1
  467 + phagen['emax'] = 100.0
  468 + phagen['enumbins'] = self._nbins
  469 + phagen['inexclusion'] = self._exclusion
  470 + phagen['bkgmethod'] = 'OFF'
  471 + phagen['etruemin'] = 0.05
  472 + phagen['etruemax'] = 150.0
  473 + phagen['etruebins'] = 5
  474 + phagen['stack'] = False
  475 + phagen['outobs'] = 'csphagen_py10_obs.xml'
  476 + phagen['outmodel'] = 'csphagen_py10_model.xml'
  477 + phagen['prefix'] = 'csphagen_py10'
  478 + phagen['logfile'] = 'csphagen_py10.log'
  479 + phagen['chatter'] = 2
  480 +
  481 + # Execute script
  482 + phagen.execute()
  483 +
  484 + # Check output
  485 + self._check_output('csphagen_py10', self._nbins, 1)
  486 + self._check_outobs('csphagen_py10_obs.xml', 1)
  487 +
  488 + # Test with multiple input observations and dedicated Off runs
  489 + # Create Off observation container
  490 + obs_off = gammalib.GObservations()
  491 + for s, events in enumerate([self._offevents, self._offevents]):
  492 + run = gammalib.GCTAObservation(events)
  493 + run.id(str(s + 1))
  494 + run.response(self._irf, gammalib.GCaldb('cta', self._caldb))
  495 + obs_off.append(run)
  496 +
  497 + # Setup csphagen
  498 + phagen = cscripts.csphagen(obs)
  499 + phagen.obs_off(obs_off)
  500 + phagen['inmodel'] = 'NONE'
  501 + phagen['srcshape'] = 'CIRCLE'
  502 + phagen['coordsys'] = 'CEL'
  503 + phagen['ra'] = 83.633
  504 + phagen['dec'] = 22.0145
  505 + phagen['rad'] = 0.2
  506 + phagen['ebinalg'] = 'LOG'
  507 + phagen['emin'] = 0.1
  508 + phagen['emax'] = 100.0
  509 + phagen['enumbins'] = self._nbins
  510 + phagen['bkgmethod'] = 'OFF'
  511 + phagen['etruemin'] = 0.05
  512 + phagen['etruemax'] = 150.0
  513 + phagen['etruebins'] = 5
  514 + phagen['stack'] = False
  515 + phagen['outobs'] = 'csphagen_py11_obs.xml'
  516 + phagen['outmodel'] = 'csphagen_py11_model.xml'
  517 + phagen['prefix'] = 'csphagen_py11'
  518 + phagen['logfile'] = 'csphagen_py11.log'
  519 + phagen['chatter'] = 2
  520 +
  521 + # Execute script
  522 + phagen.execute()
  523 +
  524 + # Check output
  525 + self._check_output('csphagen_py11_1', self._nbins, 1)
  526 + self._check_output('csphagen_py11_2', self._nbins, 1)
  527 + self._check_outobs('csphagen_py11_obs.xml', 2)
  528 +
417 # Return 529 # Return
418 return 530 return
419 531