Commit b7130ada1b6587ebddc953ee574551379de51e87

Authored by Jürgen Knödlseder
1 parent 60b94c5c

Add "NODES" method to csspec to support non-CTA analysis (#1927)

ChangeLog
... ... @@ -9,6 +9,7 @@
9 9 Add keywords to sky maps produced by ctskymap (#2194)
10 10 Enhance efficiency of computations in ctmodel (#2189)
11 11 Add csmodelsois script (#2180)
  12 + Add "NODES" method to csspec to support non-CTA analysis (#1927)
12 13 Use t-type parameters for all "tmin" and "tmax" parameters (#1864)
13 14  
14 15  
... ...
... ... @@ -140,6 +140,13 @@ for classical On/Off IACT analysis (#2208).
140 140  
141 141 The csspec script now can also handle On/Off spectra for SED generation (#2231).
142 142  
  143 +The csspec script now can also handle non-CTA data. For that purpose a "method"
  144 +parameter has been introduced that allows to selected between slicing of the data
  145 +(method="SLICE"), which is only supported for CTA observations, and replacing of
  146 +the spectral model by a node function (method="NODES"). If method="AUTO" the tool
  147 +will use "SLICE" for observations containing only CTA data and "NODES" for observations
  148 +containing other data (#1927).
  149 +
143 150  
144 151 Examples
145 152 --------
... ...
cscripts/csspec.par
... ... @@ -31,6 +31,7 @@ outfile, f, a, spectrum.fits,,, "Output spectrum file"
31 31 #
32 32 # Script parameters
33 33 #==================
  34 +method, s, a, AUTO,SLICE|NODES|AUTO,, "Spectrum generation method"
34 35 emin, r, a, 0.1,,, "Lower energy limit (TeV)"
35 36 emax, r, a, 100.0,,, "Upper energy limit (TeV)"
36 37 enumbins, i, a, 20,,, "Number of energy bins"
... ...
cscripts/csspec.py
... ... @@ -29,7 +29,7 @@ import ctools
29 29 class csspec(ctools.cscript):
30 30 """
31 31 Generates a spectrum
32   -
  32 +
33 33 This class implements the generation of a spectral energy distribution
34 34 from data.
35 35 """
... ... @@ -48,6 +48,7 @@ class csspec(ctools.cscript):
48 48 self._fits = None
49 49 self._binned_mode = False
50 50 self._onoff_mode = False
  51 + self._method = 'AUTO'
51 52  
52 53 # Initialise observation container from constructor arguments
53 54 self._obs, argv = self._set_input_obs(argv)
... ... @@ -75,16 +76,6 @@ class csspec(ctools.cscript):
75 76 if self._obs == None or self._obs.size() == 0:
76 77 self._require_inobs('csspec::get_parameters()')
77 78 self._obs = self._get_observations()
78   - # if we have one binned CTA observation, i.e. if we are in
79   - # binned mode
80   - #self._binned_mode = False
81   - #self._onoff_mode = False
82   - #if self._obs.size() == 1:
83   - # if self._obs[0].classname() == 'GCTAObservation':
84   - # if self._obs[0].eventtype() == 'CountsCube':
85   - # self._binned_mode = True
86   - # elif self._obs[0].classname() == 'GCTAOnOffObservation':
87   - # self._onoff_mode = True
88 79  
89 80 # Set models if we have none
90 81 if self._obs.models().size() == 0:
... ... @@ -106,6 +97,8 @@ class csspec(ctools.cscript):
106 97 n_unbinned += 1
107 98 elif obs.classname() == 'GCTAOnOffObservation':
108 99 n_onoff += 1
  100 + n_cta = n_unbinned + n_binned + n_onoff
  101 + n_other = self._obs.size() - n_cta
109 102  
110 103 # Set script mode according to CTA observations
111 104 if n_unbinned == 0 and n_binned != 0 and n_onoff == 0:
... ... @@ -126,6 +119,9 @@ class csspec(ctools.cscript):
126 119 # Set ebounds
127 120 self._set_ebounds()
128 121  
  122 + # Get method
  123 + self._method = self['method'].string()
  124 +
129 125 # Query other parameeters
130 126 self['edisp'].boolean()
131 127 self['calc_ulim'].boolean()
... ... @@ -140,6 +136,35 @@ class csspec(ctools.cscript):
140 136 # Write input parameters into logger
141 137 self._log_parameters(gammalib.TERSE)
142 138  
  139 + # Write spectrum method header and parameters
  140 + self._log_header1(gammalib.TERSE, 'Spectrum method')
  141 + self._log_value(gammalib.TERSE, 'Unbinned CTA observations', n_unbinned)
  142 + self._log_value(gammalib.TERSE, 'Binned CTA observations', n_binned)
  143 + self._log_value(gammalib.TERSE, 'On/off CTA observations', n_onoff)
  144 + self._log_value(gammalib.TERSE, 'Other observations', n_other)
  145 +
  146 + # If there are a mix of CTA and non-CTA observations and the method
  147 + # is 'SLICE' then log a warning that non-CTA observations will be
  148 + # ignored
  149 + if n_cta > 0 and n_other > 0 and self._method == 'SLICE':
  150 + self._log_string(TERSE, '\nWARNING: Only CTA observation can be '
  151 + 'handled with the "SLICE" method, all '
  152 + 'non-CTA observation will be ignored.')
  153 +
  154 + # If there are only non-CTA observations and the method is 'SLICE'
  155 + # then stop now
  156 + elif n_other > 0:
  157 + if self._method == 'SLICE':
  158 + msg = 'Selected "SLICE" method but none of the observations ' \
  159 + 'is a CTA observation. Please select "AUTO" or "NODES" ' \
  160 + 'if no CTA observation is provided.'
  161 + raise RuntimeError(msg)
  162 + else:
  163 + self._method = 'NODES'
  164 +
  165 + # Log selected spectrum method
  166 + self._log_value(gammalib.TERSE, 'Selected spectrum method', self._method)
  167 +
143 168 # Return
144 169 return
145 170  
... ... @@ -311,6 +336,56 @@ class csspec(ctools.cscript):
311 336 # Return
312 337 return
313 338  
  339 + def _set_replace_src_spectrum_by_nodes(self):
  340 + """
  341 + Replace source spectrum by node function
  342 + """
  343 + # Initialise model container
  344 + models = gammalib.GModels()
  345 +
  346 + # Loop over model containers
  347 + for model in self._obs.models():
  348 +
  349 + # If we deal with source model then replace the spectral model
  350 + # by a node function
  351 + if model.name() == self['srcname'].string():
  352 +
  353 + # Setup energies at log mean energies of bins
  354 + energies = gammalib.GEnergies()
  355 + for i in range(self._ebounds.size()):
  356 + energies.append(self._ebounds.elogmean(i))
  357 +
  358 + # Setup spectral node function
  359 + spectrum = gammalib.GModelSpectralNodes(model.spectral(), energies)
  360 + spectrum.autoscale()
  361 +
  362 + # Make sure that all nodes are positive
  363 + for i in range(spectrum.nodes()):
  364 + par = spectrum[i*2+1]
  365 + value = par.value()
  366 + min = 1.0e-30 * value
  367 + if min <= 0.0:
  368 + min = 1.0e-50
  369 + if min < value:
  370 + value = min
  371 + par.value(value)
  372 + par.min(min)
  373 +
  374 + # Set spectral component of source model
  375 + model.spectral(spectrum)
  376 +
  377 + # Append model
  378 + models.append(model)
  379 +
  380 + # ... otherwise just append model
  381 + else:
  382 + models.append(model)
  383 +
  384 + # Put new model in observation containers
  385 + self._obs.models(models)
  386 +
  387 + # Return
  388 + return
314 389  
315 390 def _select_onoff_obs(self, obs, emin, emax):
316 391 """
... ... @@ -444,10 +519,10 @@ class csspec(ctools.cscript):
444 519  
445 520 # Use ctselect for unbinned analysis
446 521 else:
447   -
  522 +
448 523 # Write header
449 524 self._log_header3(gammalib.EXPLICIT, 'Selecting events')
450   -
  525 +
451 526 # Select events
452 527 select = ctools.ctselect(self._obs)
453 528 select['ra'] = 'UNDEFINED'
... ... @@ -458,7 +533,7 @@ class csspec(ctools.cscript):
458 533 select['tmin'] = 'UNDEFINED'
459 534 select['tmax'] = 'UNDEFINED'
460 535 select.run()
461   -
  536 +
462 537 # Retrieve observation
463 538 obs = select.obs().copy()
464 539  
... ... @@ -610,6 +685,95 @@ class csspec(ctools.cscript):
610 685 # Return results
611 686 return results
612 687  
  688 + def _fit_model(self):
  689 + """
  690 + Fit model to observations
  691 +
  692 + Returns
  693 + -------
  694 + results : list of dict
  695 + List of dictionaries with fit results
  696 + """
  697 + # Write header
  698 + self._log_header1(gammalib.TERSE, 'Generate spectrum')
  699 +
  700 + # Write header for fitting
  701 + self._log_header3(gammalib.EXPLICIT, 'Performing fit')
  702 +
  703 + # Perform maximum likelihood fit
  704 + like = ctools.ctlike(self._obs)
  705 + like['edisp'] = self['edisp'].boolean()
  706 + like.run()
  707 +
  708 + # Initialise fit results
  709 + results = []
  710 +
  711 + # Extract fit results
  712 + model = like.obs().models()[self['srcname'].string()]
  713 + spectrum = model.spectral()
  714 + ts = model.ts()
  715 +
  716 + # Loop over all nodes
  717 + for i in range(spectrum.nodes()):
  718 +
  719 + # Get energy boundaries
  720 + emin = self._ebounds.emin(i)
  721 + emax = self._ebounds.emax(i)
  722 + elogmean = self._ebounds.elogmean(i)
  723 +
  724 + # Initialise dictionary
  725 + result = {'energy': elogmean.TeV(),
  726 + 'energy_low': (elogmean - emin).TeV(),
  727 + 'energy_high': (emax - elogmean).TeV(),
  728 + 'flux': 0.0,
  729 + 'flux_err': 0.0,
  730 + 'TS': 0.0,
  731 + 'ulimit': 0.0,
  732 + 'Npred': 0.0}
  733 +
  734 + # Convert differential flux and flux error to nuFnu
  735 + norm = elogmean.MeV() * elogmean.MeV() * gammalib.MeV2erg
  736 + result['flux'] = spectrum[i*2+1].value() * norm
  737 + result['flux_err'] = spectrum[i*2+1].error() * norm
  738 +
  739 + # Compute TS
  740 + if self['calc_ts'].boolean():
  741 +
  742 + # Copy observation container
  743 + obs = like.obs().copy()
  744 +
  745 + # Set intensity of node to tiny value
  746 + par = obs.models()[self['srcname'].string()].spectral()[i*2+1]
  747 + value = 1.0e-30 * par.value()
  748 + if par.min() > value:
  749 + par.min(value)
  750 + par.value(value)
  751 + par.fix()
  752 +
  753 + # Perform maximum likelihood fit
  754 + tslike = ctools.ctlike(obs)
  755 + tslike['edisp'] = self['edisp'].boolean()
  756 + tslike.run()
  757 +
  758 + # Store Test Statistic
  759 + model = tslike.obs().models()[self['srcname'].string()]
  760 + result['TS'] = ts - model.ts()
  761 +
  762 + # Log information
  763 + value = '%e +/- %e' % (result['flux'], result['flux_err'])
  764 + if self['calc_ulim'].boolean() and result['ulimit'] > 0.0:
  765 + value += ' [< %e]' % (result['ulimit'])
  766 + value += ' erg/cm2/s'
  767 + if self['calc_ts'].boolean() and result['TS'] > 0.0:
  768 + value += ' (TS = %.3f)' % (result['TS'])
  769 + self._log_value(gammalib.TERSE, 'Bin '+str(i+1), value)
  770 +
  771 + # Append results
  772 + results.append(result)
  773 +
  774 + # Return results
  775 + return results
  776 +
613 777 def _create_fits(self, results):
614 778 """
615 779 Create FITS file
... ... @@ -688,14 +852,26 @@ class csspec(ctools.cscript):
688 852 # Write input observation container into logger
689 853 self._log_observations(gammalib.NORMAL, self._obs, 'Input observation')
690 854  
  855 + # Write spectral binning into logger
  856 + self._log_spectral_binning()
  857 +
691 858 # Adjust model parameters dependent on input user parameters
692 859 self._adjust_model_pars()
693 860  
694   - # Write spectral binning into logger
695   - self._log_spectral_binning()
  861 + # Case A: "SLICE" method
  862 + if self._method == 'SLICE':
696 863  
697   - # Fit energy bins
698   - results = self._fit_energy_bins()
  864 + # Fit energy bins
  865 + results = self._fit_energy_bins()
  866 +
  867 + # Case B: "NODES" method
  868 + elif self._method == 'NODES':
  869 +
  870 + # Replace source spectrum by nodes function
  871 + self._set_replace_src_spectrum_by_nodes()
  872 +
  873 + # Fit model
  874 + results = self._fit_model()
699 875  
700 876 # Create FITS file
701 877 self._create_fits(results)
... ... @@ -738,7 +914,7 @@ class csspec(ctools.cscript):
738 914  
739 915 # Get outmap parameter
740 916 outfile = self['outfile'].filename()
741   -
  917 +
742 918 # Log file name
743 919 self._log_value(gammalib.NORMAL, 'Spectrum file', outfile.url())
744 920  
... ...
doc/source/admin/release_history/1.5.rst
... ... @@ -31,6 +31,8 @@ Improvements
31 31 Add keywords to sky maps produced by :ref:`ctskymap`
32 32 * [`2189 <https://cta-redmine.irap.omp.eu/issues/2189>`_] -
33 33 Enhance efficiency of computations in :ref:`ctmodel`
  34 +* [`1927 <https://cta-redmine.irap.omp.eu/issues/1927>`_] -
  35 + Add ``NODES`` method to :ref:`csspec` to support non-CTA analysis
34 36  
35 37  
36 38 New features
... ...
doc/source/users/reference_manual/csspec.rst
... ... @@ -79,8 +79,13 @@ General parameters
79 79 Name of the source in the source model XML file which should be used
80 80 for sensitivity computation.
81 81  
82   -``outfile [file]``
83   - Output source spectrum file.
  82 +``outfile <SLICE|NODES|AUTO> [file]``
  83 + Spectrum generation method.
  84 + ``SLICE``: Slice energy interval into ``enumbins`` independent bins.
  85 + ``NODES``: Replace spectral model by node function with ``enumbins`` nodes.
  86 + ``AUTO`` : Automatically select method based on input data. Will use ``SLICE``
  87 + for CTA-only observations and ``NODES`` for observations including
  88 + non-CTA data.
84 89  
85 90 ``emin [real]``
86 91 Lower energy limit of events (in TeV).
... ...