ctools  2.0.0
 All Classes Namespaces Files Functions Variables Macros Pages
csspec.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 # ==========================================================================
3 # Generates a spectrum.
4 #
5 # Copyright (C) 2014-2022 Michael Mayer
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 tempfile # Kludge for file function generation
24 import gammalib
25 import ctools
26 from cscripts import mputils
27 
28 
29 # ============ #
30 # csspec class #
31 # ============ #
32 class csspec(ctools.csobservation):
33  """
34  Generates a spectrum
35 
36  This class implements the generation of a Spectral Energy Distribution
37  (SED) from gamma-ray observations.
38  """
39 
40  # Constructors and destructors
41  def __init__(self, *argv):
42  """
43  Constructor
44  """
45  # Initialise application by calling the appropriate class constructor
46  self._init_csobservation(self.__class__.__name__, ctools.__version__, argv)
47 
48  # Initialise data members
49  self._ebounds = gammalib.GEbounds()
50  self._fits = None
51  self._binned_mode = False
52  self._onoff_mode = False
53  self._method = 'AUTO'
54  self._nthreads = 0
55 
56  # Return
57  return
58 
59  def __del__(self):
60  """
61  Destructor
62  """
63  # Return
64  return
65 
66  # State methods for pickling
67  def __getstate__(self):
68  """
69  Extend ctools.csobservation getstate method to include some members
70 
71  Returns
72  -------
73  state : dict
74  Pickled instance
75  """
76  # Set pickled dictionary
77  state = {'base' : ctools.csobservation.__getstate__(self),
78  'ebounds' : self._ebounds,
79  'fits' : self._fits,
80  'binned_mode' : self._binned_mode,
81  'onoff_mode' : self._onoff_mode,
82  'method' : self._method,
83  'nthreads' : self._nthreads}
84 
85  # Return pickled dictionary
86  return state
87 
88  def __setstate__(self, state):
89  """
90  Extend ctools.csobservation setstate method to include some members
91 
92  Parameters
93  ----------
94  state : dict
95  Pickled instance
96  """
97  ctools.csobservation.__setstate__(self, state['base'])
98  self._ebounds = state['ebounds']
99  self._fits = state['fits']
100  self._binned_mode = state['binned_mode']
101  self._onoff_mode = state['onoff_mode']
102  self._method = state['method']
103  self._nthreads = state['nthreads']
104 
105  # Return
106  return
107 
108 
109  # Private methods
110  def _get_parameters(self):
111  """
112  Get parameters from parfile and setup the observation
113  """
114  # Set observation if not done before
115  if self.obs().is_empty():
116  self._require_inobs('csspec::get_parameters()')
117  self.obs(self._get_observations())
118 
119  # Set observation statistic
120  self._set_obs_statistic(gammalib.toupper(self['statistic'].string()))
121 
122  # Set models if we have none
123  if self.obs().models().is_empty():
124  self.obs().models(self['inmodel'].filename())
125 
126  # Query source name
127  self['srcname'].string()
128 
129  # Get spectrum generation method
130  self._method = self['method'].string()
131 
132  # Collect number of unbinned, binned and On/Off observations in
133  # observation container
134  n_unbinned = 0
135  n_binned = 0
136  n_onoff = 0
137  for obs in self.obs():
138  if obs.classname() == 'GCTAObservation':
139  if obs.eventtype() == 'CountsCube':
140  n_binned += 1
141  else:
142  n_unbinned += 1
143  elif obs.classname() == 'GCTAOnOffObservation':
144  n_onoff += 1
145  n_cta = n_unbinned + n_binned + n_onoff
146  n_other = self.obs().size() - n_cta
147 
148  # If spectrum method is not "NODES" or 'BINS" then set spectrum method
149  # and script mode according to type of observations
150  if self._method != 'NODES' and self._method != 'BINS':
151  if n_other > 0:
152  self._method = 'NODES'
153  else:
154  if n_unbinned == 0 and n_binned != 0 and n_onoff == 0:
155  self._binned_mode = True
156  self._method = 'SLICE'
157  elif n_unbinned == 0 and n_binned == 0 and n_onoff != 0:
158  self._onoff_mode = True
159  self._method = 'SLICE'
160  elif n_unbinned == 0 and n_binned != 0 and n_onoff != 0:
161  msg = 'Mix of binned and On/Off CTA observations found ' \
162  'in observation container. csscript does not support ' \
163  'this mix.'
164  raise RuntimeError(msg)
165  elif n_unbinned != 0 and (n_binned != 0 or n_onoff != 0):
166  msg = 'Mix of unbinned and binned or On/Off CTA observations ' \
167  'found in observation container. csscript does not ' \
168  'support this mix.'
169  raise RuntimeError(msg)
170  elif n_unbinned != 0:
171  self._method = 'SLICE'
172 
173  # Set ebounds
174  self._set_ebounds()
175 
176  # Query other parameeters
177  self['edisp'].boolean()
178  self['calc_ts'].boolean()
179  self['calc_ulim'].boolean()
180  self['confidence'].real()
181  self['fix_bkg'].boolean()
182  self['fix_srcs'].boolean()
183  self['bingamma'].real()
184 
185  # Setup dlog-likelihood parameters
186  self['dll_sigstep'].real()
187  self['dll_sigmax'].real()
188  self['dll_freenodes'].boolean()
189 
190  # Read ahead output parameters
191  if self._read_ahead():
192  self['outfile'].filename()
193 
194  # Write input parameters into logger
195  self._log_parameters(gammalib.TERSE)
196 
197  # Set number of processes for multiprocessing
198  self._nthreads = mputils.nthreads(self)
199 
200  # Write spectrum method header and parameters
201  self._log_header1(gammalib.TERSE, 'Spectrum method')
202  self._log_value(gammalib.TERSE, 'Unbinned CTA observations', n_unbinned)
203  self._log_value(gammalib.TERSE, 'Binned CTA observations', n_binned)
204  self._log_value(gammalib.TERSE, 'On/off CTA observations', n_onoff)
205  self._log_value(gammalib.TERSE, 'Other observations', n_other)
206 
207  # If there are a mix of CTA and non-CTA observations and the method
208  # is 'SLICE' then log a warning that non-CTA observations will be
209  # ignored
210  warning = False
211  if n_cta > 0 and n_other > 0 and self._method == 'SLICE':
212  warning = True
213 
214  # If there are only non-CTA observations and the method is 'SLICE'
215  # then stop now
216  elif n_other > 0:
217  if self._method == 'SLICE':
218  msg = 'Selected "SLICE" method but none of the observations ' \
219  'is a CTA observation. Please select "AUTO", "NODES" ' \
220  'or "BINS" if no CTA observation is provided.'
221  raise RuntimeError(msg)
222  elif self._method == 'AUTO':
223  self._method = 'NODES'
224 
225  # Log selected spectrum method
226  self._log_value(gammalib.TERSE, 'Selected spectrum method', self._method)
227 
228  # Signal warning
229  if warning:
230  self._log_string(gammalib.TERSE, ' WARNING: Only CTA observation '
231  'can be handled with the "SLICE" method, all '
232  'non-CTA observation will be ignored.')
233 
234  # Return
235  return
236 
237  def _set_ebounds(self):
238  """
239  Set energy boundaries
240  """
241  # If we are in binned or On/Off mode then align the energy boundaries
242  # with the cube of RMF spectrum
243  if self._binned_mode or self._onoff_mode:
244 
245  # Initialise energy boundaries for spectrum
246  self._ebounds = gammalib.GEbounds()
247 
248  # Create energy boundaries according to user parameters
249  ebounds = self._create_ebounds()
250 
251  # Extract binned energy boundaries from first observation in
252  # container. This assumes that all observations have the same
253  # energy binning. But even if this is not the case, the script
254  # should work reasonably well since for each observation the
255  # relevant energy bins will be selected.
256  if self._binned_mode:
257  cube_ebounds = self.obs()[0].events().ebounds()
258  else:
259  cube_ebounds = self.obs()[0].rmf().emeasured()
260 
261  # Loop over user energy boundaries and collect all energy bins
262  # that overlap
263  for i in range(ebounds.size()):
264 
265  # Extract minimum and maximum energy of user energy bin,
266  # including some rounding tolerance
267  emin = ebounds.emin(i).TeV() * 0.999 # Rounding tolerance
268  emax = ebounds.emax(i).TeV() * 1.001 # Rounding tolerance
269 
270  # Set number of overlapping energy bins
271  nbins = 0
272 
273  # Search first cube bin that is comprised within user energy
274  # bin
275  emin_value = -1.0
276  for k in range(cube_ebounds.size()):
277  if cube_ebounds.emin(k).TeV() >= emin and \
278  cube_ebounds.emax(k).TeV() <= emax:
279  emin_value = cube_ebounds.emin(k).TeV()
280  break
281  if emin_value < 0.0:
282  continue
283 
284  # Search last cube bin that is comprised within user energy bin
285  for k in range(cube_ebounds.size()):
286  if cube_ebounds.emin(k).TeV() >= emin and \
287  cube_ebounds.emax(k).TeV() <= emax:
288  emax_value = cube_ebounds.emax(k).TeV()
289  nbins += 1
290 
291  # Append energy bin if there are overlapping bins in the
292  # counts cube
293  if nbins > 0:
294  self._ebounds.append(gammalib.GEnergy(emin_value, 'TeV'),
295  gammalib.GEnergy(emax_value, 'TeV'))
296 
297  # Raise an exception if there are no overlapping layers
298  if (len(self._ebounds) == 0):
299  msg = 'Energy range ['+str(cube_ebounds.emin())+ \
300  ', '+str(cube_ebounds.emax())+'] of counts '+ \
301  'cube does not overlap with specified energy '+ \
302  'range ['+ \
303  str(ebounds.emin())+', '+str(ebounds.emax())+'].'+ \
304  ' Specify overlapping energy range.'
305  raise RuntimeError(msg)
306 
307  # Unbinned mode
308  else:
309  self._ebounds = self._create_ebounds()
310 
311  # Return
312  return
313 
315  """
316  Log spectral binning
317  """
318  # Write header
319  self._log_header1(gammalib.TERSE, 'Spectral binning')
320 
321  # Log counts cube energy range for binned mode
322  if self._binned_mode:
323  cube_ebounds = self.obs()[0].events().ebounds()
324  value = '%s - %s' % (str(cube_ebounds.emin()),
325  str(cube_ebounds.emax()))
326  self._log_value(gammalib.TERSE, 'Counts cube energy range', value)
327 
328  # Log RMF energy range for On/Off mode
329  elif self._onoff_mode:
330  etrue = self.obs()[0].rmf().etrue()
331  ereco = self.obs()[0].rmf().emeasured()
332  vtrue = '%s - %s' % (str(etrue.emin()), str(etrue.emax()))
333  vreco = '%s - %s' % (str(ereco.emin()), str(ereco.emax()))
334  self._log_value(gammalib.TERSE, 'RMF true energy range', vtrue)
335  self._log_value(gammalib.TERSE, 'RMF measured energy range', vreco)
336 
337  # Log energy bins
338  for i in range(self._ebounds.size()):
339  value = '%s - %s' % (str(self._ebounds.emin(i)),
340  str(self._ebounds.emax(i)))
341  self._log_value(gammalib.TERSE, 'Bin %d' % (i+1), value)
342 
343  # Return
344  return
345 
347  """
348  Adjust model parameters
349  """
350  # Write header
351  self._log_header1(gammalib.TERSE, 'Adjust model parameters')
352 
353  # Adjust model parameters dependent on input user parameters
354  for model in self.obs().models():
355 
356  # Initialise TS flag for all models to false
357  model.tscalc(False)
358 
359  # Log model name
360  self._log_header3(gammalib.EXPLICIT, model.name())
361 
362  # Deal with the source of interest
363  if model.name() == self['srcname'].string():
364 
365  # Fix all model parameters
366  for par in model:
367  if par.is_free():
368  self._log_string(gammalib.EXPLICIT,
369  ' Fixing "'+par.name()+'"')
370  par.fix()
371 
372  # Convert spectral model into a file function. The file
373  # function is logarithmically sampled in energy, with with
374  # 20 times the number of spectral bins
375  nbins = self._ebounds.size()
376  num = nbins * 50
377  energies = gammalib.GEnergies(num, self._ebounds.emin(0),
378  self._ebounds.emax(nbins-1))
379  spectral = gammalib.GModelSpectralFunc(model.spectral(), energies)
380  model.spectral(spectral)
381  self._log_string(gammalib.EXPLICIT, ' Converting spectral model '
382  'into file function')
383 
384  # Free the normalisation parameter which is assumed to be
385  # the first spectral parameter
386  normpar = model.spectral()[0]
387  if normpar.is_fixed():
388  self._log_string(gammalib.EXPLICIT,
389  ' Freeing "'+normpar.name()+'"')
390  normpar.free()
391 
392  # Optionally compute Test Statistic value
393  if self['calc_ts'].boolean():
394  model.tscalc(True)
395 
396  # Deal with background models
397  elif self['fix_bkg'].boolean() and \
398  not model.classname() == 'GModelSky':
399  for par in model:
400  if par.is_free():
401  self._log_string(gammalib.EXPLICIT,
402  ' Fixing "'+par.name()+'"')
403  par.fix()
404 
405  # Deal with source models
406  elif self['fix_srcs'].boolean() and \
407  model.classname() == 'GModelSky':
408  for par in model:
409  if par.is_free():
410  self._log_string(gammalib.EXPLICIT,
411  ' Fixing "'+par.name()+'"')
412  par.fix()
413 
414  # Return
415  return
416 
418  """
419  Replace source spectrum by node function
420  """
421  # Initialise model container
422  models = gammalib.GModels()
423 
424  # Loop over model containers
425  for model in self.obs().models():
426 
427  # If we deal with source model then replace the spectral model
428  # by a node function
429  if model.name() == self['srcname'].string():
430 
431  # Setup energies at log mean energies of bins
432  energies = gammalib.GEnergies()
433  for i in range(self._ebounds.size()):
434  energies.append(self._ebounds.elogmean(i))
435 
436  # Setup spectral node function
437  spectrum = gammalib.GModelSpectralNodes(model.spectral(), energies)
438  spectrum.autoscale()
439 
440  # Make sure that all nodes are positive. Autoscale all
441  # parameters so that their nominal value is unity.
442  for i in range(spectrum.nodes()):
443  par = spectrum[i*2+1]
444  par.autoscale()
445  value = par.value()
446  minimum = 1.0e-20 * value
447  if minimum <= 0.0:
448  minimum = 1.0e-20
449  if minimum < value:
450  value = minimum
451  par.value(value)
452  par.min(minimum)
453 
454  # Set spectral component of source model
455  model.spectral(spectrum)
456 
457  # Make sure that TS computation is disabled (makes computation
458  # faster)
459  model.tscalc(False)
460 
461  # Append model
462  models.append(model)
463 
464  # ... otherwise just append model
465  else:
466  models.append(model)
467 
468  # Put new model in observation containers
469  self.obs().models(models)
470 
471  # Return
472  return
473 
475  """
476  Replace source spectrum by bin function
477  """
478  # Initialise model container
479  models = gammalib.GModels()
480 
481  # Loop over model containers
482  for model in self.obs().models():
483 
484  # If we deal with source model then replace the spectral model
485  # by a bin function
486  if model.name() == self['srcname'].string():
487 
488  # Get spectral bins index
489  bingamma = self['bingamma'].real()
490 
491  # Setup spectral bin function
492  spectrum = gammalib.GModelSpectralBins(model.spectral(),
493  self._ebounds,
494  bingamma)
495  spectrum.autoscale()
496 
497  # Make sure that all bins are positive. Autoscale all
498  # parameters so that their nominal value is unity.
499  for i in range(spectrum.bins()):
500  name = 'Intensity%d' % i
501  par = spectrum[name]
502  par.autoscale()
503  value = par.value()
504  minimum = 1.0e-20 * value
505  if minimum <= 0.0:
506  minimum = 1.0e-20
507  if minimum < value:
508  value = minimum
509  par.value(value)
510  par.min(minimum)
511 
512  # Set spectral component of source model
513  model.spectral(spectrum)
514 
515  # Make sure that TS computation is disabled (makes computation
516  # faster)
517  model.tscalc(False)
518 
519  # Append model
520  models.append(model)
521 
522  # ... otherwise just append model
523  else:
524  models.append(model)
525 
526  # Put new model in observation containers
527  self.obs().models(models)
528 
529  # Return
530  return
531 
532  def _select_onoff_obs(self, obs, emin, emax):
533  """
534  Select an energy interval from one CTA On/Off observation
535 
536  Parameters
537  ----------
538  obs : `~gammalib.GCTAOnOffObservation`
539  Minimum energy
540  emin : `~gammalib.GEnergy()`
541  Minimum energy
542  emax : `~gammalib.GEnergy()`
543  Maximum energy
544 
545  Returns
546  -------
547  obs : `~gammalib.GCTAOnOffObservation`
548  CTA On/Off observation
549  """
550  # Select energy bins in etrue and ereco. All etrue energy bins are
551  # selected. A 0.1% margin is added for reconstructed energies to
552  # accomodate for rounding errors.
553  etrue = obs.rmf().etrue()
554  ereco = gammalib.GEbounds()
555  itrue = [i for i in range(obs.rmf().etrue().size())]
556  ireco = []
557  for i in range(obs.rmf().emeasured().size()):
558  ereco_bin_min = obs.rmf().emeasured().emin(i)
559  ereco_bin_max = obs.rmf().emeasured().emax(i)
560  if ereco_bin_min * 1.001 >= emin and ereco_bin_max * 0.999 <= emax:
561  ereco.append(ereco_bin_min, ereco_bin_max)
562  ireco.append(i)
563 
564  # Extract PHA
565  pha_on = gammalib.GPha(ereco)
566  pha_off = gammalib.GPha(ereco)
567  pha_on.exposure(obs.on_spec().exposure())
568  pha_off.exposure(obs.on_spec().exposure())
569  for idst, isrc in enumerate(ireco):
570  # On
571  pha_on[idst] = obs.on_spec()[isrc]
572  pha_on.areascal(idst, obs.on_spec().areascal(isrc))
573  pha_on.backscal(idst, obs.on_spec().backscal(isrc))
574  # Off
575  pha_off[idst] = obs.off_spec()[isrc]
576  pha_off.areascal(idst, obs.off_spec().areascal(isrc))
577  pha_off.backscal(idst, obs.off_spec().backscal(isrc))
578 
579  # Extract BACKRESP
580  pha_backresp = obs.off_spec()['BACKRESP']
581  backresp = []
582  for idst, isrc in enumerate(ireco):
583  backresp.append(pha_backresp[isrc])
584  pha_off.append('BACKRESP', backresp)
585 
586  # Extract ARF
587  arf = gammalib.GArf(etrue)
588  for idst, isrc in enumerate(itrue):
589  arf[idst] = obs.arf()[isrc]
590 
591  # Extract RMF
592  rmf = gammalib.GRmf(etrue, ereco)
593  for idst_true, isrc_true in enumerate(itrue):
594  for idst_reco, isrc_reco in enumerate(ireco):
595  rmf[idst_true, idst_reco] = obs.rmf()[isrc_true, isrc_reco]
596 
597  # Set On/Off observations
598  obsid = obs.id()
599  statistic = obs.statistic()
600  instrument = obs.instrument()
601  obs = gammalib.GCTAOnOffObservation(pha_on, pha_off, arf, rmf)
602  obs.id(obsid)
603  obs.statistic(statistic)
604  obs.instrument(instrument)
605 
606  # Return observation
607  return obs
608 
609  def _select_obs(self, emin, emax):
610  """
611  Select observations for energy interval
612 
613  Parameters
614  ----------
615  emin : `~gammalib.GEnergy()`
616  Minimum energy
617  emax : `~gammalib.GEnergy()`
618  Maximum energy
619 
620  Returns
621  -------
622  obs : `~gammalib.GObservations`
623  Observation container
624  """
625  # Use ctcubemask for binned analysis
626  if self._binned_mode:
627 
628  # Write header
629  self._log_header3(gammalib.EXPLICIT, 'Filtering cube')
630 
631  # Select layers
632  cubemask = ctools.ctcubemask(self.obs())
633  cubemask['regfile'] = 'NONE'
634  cubemask['ra'] = 'UNDEFINED'
635  cubemask['dec'] = 'UNDEFINED'
636  cubemask['rad'] = 'UNDEFINED'
637  cubemask['emin'] = emin.TeV()
638  cubemask['emax'] = emax.TeV()
639 
640  # If chatter level is verbose and debugging is requested then
641  # switch also on the debug model in ctcubemask
642  if self._logVerbose() and self._logDebug():
643  cubemask['debug'] = True
644 
645  # Select layers
646  cubemask.run()
647 
648  # Set new binned observation
649  obs = cubemask.obs().copy()
650 
651  # Use ...
652  elif self._onoff_mode:
653 
654  # Write header
655  self._log_header3(gammalib.EXPLICIT, 'Filtering PHA, ARF and RMF')
656 
657  # Initialise observation container
658  obs = gammalib.GObservations()
659 
660  # Loop over all input observations and select energy bins for
661  # all On/Off observations
662  for run in self.obs():
663  if run.classname() == 'GCTAOnOffObservation':
664  obs.append(self._select_onoff_obs(run, emin, emax))
665 
666  # Append models
667  obs.models(self.obs().models())
668 
669  # Use ctselect for unbinned analysis
670  else:
671 
672  # Write header
673  self._log_header3(gammalib.EXPLICIT, 'Selecting events')
674 
675  # Select events
676  select = ctools.ctselect(self.obs())
677  select['ra'] = 'UNDEFINED'
678  select['dec'] = 'UNDEFINED'
679  select['rad'] = 'UNDEFINED'
680  select['emin'] = emin.TeV()
681  select['emax'] = emax.TeV()
682  select['tmin'] = 'UNDEFINED'
683  select['tmax'] = 'UNDEFINED'
684 
685  # If chatter level is verbose and debugging is requested then
686  # switch also on the debug model in ctselect
687  if self._logVerbose() and self._logDebug():
688  select['debug'] = True
689 
690  # Run ctselect
691  select.run()
692 
693  # Retrieve observation
694  obs = select.obs().copy()
695 
696  # Return observation container
697  return obs
698 
699  def _fit_energy_bin(self, i):
700  """
701  Fit data for one energy bin
702 
703  Parameters
704  ----------
705  i : int
706  Energy bin index
707 
708  Returns
709  -------
710  result : dict
711  Dictionary with fit results
712  """
713 
714  # Write header for energy bin
715  self._log_header2(gammalib.EXPLICIT, 'Energy bin ' + str(i + 1))
716 
717  # Get energy boundaries
718  emin = self._ebounds.emin(i)
719  emax = self._ebounds.emax(i)
720  elogmean = self._ebounds.elogmean(i)
721  e_scale = elogmean.MeV() * elogmean.MeV() * gammalib.MeV2erg
722 
723  # Select observations for energy bin
724  obs = self._select_obs(emin, emax)
725 
726  # Initialise dictionary
727  result = {'energy': elogmean.TeV(),
728  'energy_low': (elogmean - emin).TeV(),
729  'energy_high': (emax - elogmean).TeV(),
730  'flux': 0.0,
731  'e2dnde': 0.0,
732  'flux_err': 0.0,
733  'TS': 0.0,
734  'ulimit': 0.0,
735  'Npred': 0.0,
736  'logL': 0.0,
737  'dloglike': [],
738  'norm_scan': []}
739 
740  # Write header for fitting
741  self._log_header3(gammalib.EXPLICIT, 'Performing fit in energy bin')
742 
743  # Setup maximum likelihood fit
744  like = ctools.ctlike(obs)
745  like['edisp'] = self['edisp'].boolean()
746  like['nthreads'] = 1 # Avoids OpenMP conflict
747 
748  # If chatter level is verbose and debugging is requested then
749  # switch also on the debug model in ctlike
750  if self._logVerbose() and self._logDebug():
751  like['debug'] = True
752 
753  # Perform maximum likelihood fit
754  like.run()
755 
756  # Write model results for explicit chatter level
757  self._log_string(gammalib.EXPLICIT, str(like.obs().models()))
758 
759  # Continue only if log-likelihood is non-zero
760  if like.obs().logL() != 0.0:
761 
762  # Get results
763  fitted_models = like.obs().models()
764  source = fitted_models[self['srcname'].string()]
765 
766  # Compute delta log-likelihood
767  if self['dll_sigstep'].real() > 0.0:
768 
769  # Extract the normalization scan values
770  parname = source.spectral()[0].name()
771  (norm_scan, dlogL, loglike) = self._profile_logL(like.copy(), parname, elogmean)
772  result['norm_scan'] = norm_scan
773  result['dloglike'] = dlogL
774  result['logL'] = loglike
775 
776  # Extract Test Statistic value
777  if self['calc_ts'].boolean():
778  result['TS'] = source.ts()
779 
780  # Compute Npred value (only works for unbinned analysis)
781  if not self._binned_mode and not self._onoff_mode:
782  for observation in like.obs():
783  result['Npred'] += observation.npred(source)
784 
785  # Compute upper flux limit
786  ulimit_value = -1.0
787  if self['calc_ulim'].boolean():
788 
789  # Logging information
790  self._log_header3(gammalib.EXPLICIT,
791  'Computing upper limit for energy bin')
792 
793  # Create upper limit object
794  ulimit = ctools.ctulimit(like.obs())
795  ulimit['srcname'] = self['srcname'].string()
796  ulimit['eref'] = elogmean.TeV()
797  ulimit['confidence'] = self['confidence'].real()
798 
799  # If chatter level is verbose and debugging is requested
800  # then switch also on the debug model in ctulimit
801  if self._logVerbose() and self._logDebug():
802  ulimit['debug'] = True
803 
804  # Try to run upper limit and catch exceptions
805  try:
806  ulimit.run()
807  ulimit_value = ulimit.diff_ulimit()
808  except:
809  self._log_string(gammalib.EXPLICIT, 'Upper limit '
810  'calculation failed.')
811  ulimit_value = -1.0
812 
813  # Compute upper limit
814  if ulimit_value > 0.0:
815  result['ulimit'] = ulimit_value
816 
817  # Compute differential flux and flux error
818  fitted_flux = source.spectral().eval(elogmean)
819  parvalue = source.spectral()[0].value()
820  if parvalue != 0.0:
821  rel_error = source.spectral()[0].error() / parvalue
822  e_flux = fitted_flux * rel_error
823  else:
824  e_flux = 0.0
825 
826  # If the source model is a cube then multiply-in the cube
827  # spectrum
828  if source.spatial().classname() == 'GModelSpatialDiffuseCube':
829  region = gammalib.GSkyRegionCircle(0.0, 0.0, 180.0)
830  source.spatial().mc_cone(region)
831  norm = source.spatial().spectrum().eval(elogmean)
832  fitted_flux *= norm
833  e_flux *= norm
834 
835  # Convert differential flux and flux error to nuFnu
836  result['flux'] = fitted_flux
837  result['e2dnde'] = fitted_flux * e_scale
838  result['flux_err'] = e_flux
839 
840  # Log information
841  value = '%e +/- %e' % (result['e2dnde'], result['flux_err']*e_scale)
842  if self['calc_ulim'].boolean() and result['ulimit'] > 0.0:
843  value += ' [< %e]' % (result['ulimit']*e_scale)
844  value += ' erg/cm2/s'
845  if self['calc_ts'].boolean() and result['TS'] > 0.0:
846  value += ' (TS = %.3f)' % (result['TS'])
847  self._log_value(gammalib.TERSE, 'Bin '+str(i+1), value)
848 
849  # ... otherwise if logL is zero then signal that bin is
850  # skipped
851  else:
852  value = 'Likelihood is zero. Bin is skipped.'
853  self._log_value(gammalib.TERSE, 'Bin '+str(i+1), value)
854 
855  # Return result
856  return result
857 
858  def _fit_energy_bins(self):
859  """
860  Fit model to energy bins
861 
862  Returns
863  -------
864  results : list of dict
865  List of dictionaries with fit results
866  """
867  # Write header
868  self._log_header1(gammalib.TERSE, 'Generate spectrum')
869  self._log_string(gammalib.TERSE, str(self._ebounds))
870 
871  # Initialise results
872  results = []
873 
874  # If more than a single thread is requested then use multiprocessing
875  if self._nthreads > 1:
876 
877  # Compute energy bins
878  args = [(self, '_fit_energy_bin', i)
879  for i in range(self._ebounds.size())]
880  poolresults = mputils.process(self._nthreads, mputils.mpfunc, args)
881 
882  # Construct results
883  for i in range(self._ebounds.size()):
884  results.append(poolresults[i][0])
885  self._log_string(gammalib.TERSE, poolresults[i][1]['log'], False)
886 
887  # Otherwise, loop over energy bins
888  else:
889  for i in range(self._ebounds.size()):
890 
891  # Fit energy bin
892  result = self._fit_energy_bin(i)
893 
894  # Append results
895  results.append(result)
896 
897  # Return results
898  return results
899 
900  def _fit_model(self):
901  """
902  Fit model to observations
903 
904  Returns
905  -------
906  results : list of dict
907  List of dictionaries with fit results
908  """
909  # Write header
910  self._log_header1(gammalib.TERSE, 'Generate spectrum')
911 
912  # Write header for fitting
913  self._log_header3(gammalib.EXPLICIT, 'Performing model fit')
914 
915  # Perform maximum likelihood fit
916  like = ctools.ctlike(self.obs())
917  like['edisp'] = self['edisp'].boolean()
918  like.run()
919 
920  # Initialise fit results
921  results = []
922 
923  # Extract fit results
924  model = like.obs().models()[self['srcname'].string()]
925  spectrum = model.spectral()
926  logL0 = like.obs().logL()
927 
928  # Write model results for explicit chatter level
929  self._log_string(gammalib.EXPLICIT, str(like.obs().models()))
930 
931  # Loop over all nodes or bins
932  for i in range(self._ebounds.size()):
933 
934  # Get energy boundaries
935  emin = self._ebounds.emin(i)
936  emax = self._ebounds.emax(i)
937  elogmean = self._ebounds.elogmean(i)
938 
939  # Initialise dictionary
940  result = {'energy': elogmean.TeV(),
941  'energy_low': (elogmean - emin).TeV(),
942  'energy_high': (emax - elogmean).TeV(),
943  'flux': 0.0,
944  'e2dnde': 0.0,
945  'flux_err': 0.0,
946  'TS': 0.0,
947  'ulimit': 0.0,
948  'Npred': 0.0,
949  'logL': 0.0,
950  'dloglike': [],
951  'norm_scan': []}
952 
953  # Convert differential flux and flux error to nuFnu
954  norm = elogmean.MeV() * elogmean.MeV() * gammalib.MeV2erg
955  result['flux'] = spectrum.intensity(i)
956  result['e2dnde'] = spectrum.intensity(i) * norm
957  result['flux_err'] = spectrum.error(i)
958 
959  # Compute upper flux limit
960  ulimit_value = -1.0
961  parname = 'Intensity%d' % i
962 
963  # Compute delta log-likelihood
964  if self['dll_sigstep'].real() > 0.0:
965 
966  # Extract the normalization scan values
967  (norm_scan, dlogL, loglike) = self._profile_logL(like.copy(),
968  parname,
969  elogmean)
970  result['norm_scan'] = norm_scan
971  result['dloglike'] = dlogL
972  result['logL'] = loglike
973 
974  # Compute upper flux limit
975  if self['calc_ulim'].boolean():
976 
977  # Logging information
978  self._log_header3(gammalib.EXPLICIT,
979  'Computing upper limit for node energy %f TeV' %
980  result['energy'])
981 
982  # Copy observation container
983  obs = like.obs().copy()
984 
985  # Fix intensities of all nodes
986  spectral = obs.models()[self['srcname'].string()].spectral()
987  for par in spectral:
988  par.fix()
989 
990  # Create upper limit object
991  ulimit = ctools.ctulimit(obs)
992  ulimit['srcname'] = self['srcname'].string()
993  ulimit['parname'] = parname
994  ulimit['eref'] = elogmean.TeV()
995  ulimit['tol'] = 1.0e-3
996 
997  # Try to run upper limit and catch exceptions
998  try:
999  ulimit.run()
1000  ulimit_value = ulimit.diff_ulimit()
1001  except:
1002  self._log_string(gammalib.EXPLICIT, 'Upper limit '
1003  'calculation failed.')
1004  ulimit_value = -1.0
1005 
1006  # Compute upper limit
1007  if ulimit_value > 0.0:
1008  result['ulimit'] = ulimit_value
1009 
1010  # Compute TS
1011  if self['calc_ts'].boolean():
1012 
1013  # Copy observation container
1014  obs = like.obs().copy()
1015 
1016  # Set intensity of node to tiny value by scaling the value
1017  # by a factor 1e-8.
1018  par = obs.models()[self['srcname'].string()].spectral()[parname]
1019  par.autoscale()
1020  par.factor_min(1.0e-8)
1021  par.factor_value(1.0e-8)
1022  par.autoscale()
1023  par.fix()
1024 
1025  # Perform maximum likelihood fit
1026  tslike = ctools.ctlike(obs)
1027  tslike['edisp'] = self['edisp'].boolean()
1028  tslike.run()
1029 
1030  # Store Test Statistic
1031  model = tslike.obs().models()[self['srcname'].string()]
1032  logL1 = tslike.obs().logL()
1033  result['TS'] = 2.0 * (logL1 - logL0)
1034 
1035  # Log information
1036  value = '%e +/- %e' % (result['e2dnde'], result['flux_err']*norm)
1037  if self['calc_ulim'].boolean() and result['ulimit'] > 0.0:
1038  value += ' [< %e]' % (result['ulimit']*norm)
1039  value += ' erg/cm2/s'
1040  if self['calc_ts'].boolean() and result['TS'] > 0.0:
1041  value += ' (TS = %.3f)' % (result['TS'])
1042  self._log_value(gammalib.TERSE, 'Bin '+str(i+1), value)
1043 
1044  # Append results
1045  results.append(result)
1046 
1047  # Return results
1048  return results
1049 
1050  def _profile_logL(self, like, parname, elogmean):
1051  """
1052  Computes the delta log-likelihood profile in a single energy bin
1053 
1054  Parameters
1055  ----------
1056  like : `~ctools.ctlike()`
1057  ctlike fitter containing prefit model
1058  parname : str
1059  Name of the spectral parameter to be fit
1060  elogmean : `~gammalib.GEnergy()`
1061  Energy at which the model is to be evaluated
1062 
1063  Returns
1064  -------
1065  norm_scan : list
1066  Normalization values
1067  dloglike_scan : list
1068  Computed loglikelihood values
1069  loglike: float
1070  Computed reference log-likelihood for dloglike_scan
1071  """
1072  # Compute number of steps
1073  dll_sigmax = self['dll_sigmax'].real()
1074  dll_sigstep = self['dll_sigstep'].real()
1075  sigsteps = int(2 * (dll_sigmax/dll_sigstep) + 1)
1076 
1077  # Setup the source model for fitting
1078  source = like.obs().models()[self['srcname'].string()]
1079  spectral = source.spectral()
1080  flux_par = spectral[parname]
1081  norm = flux_par.value()
1082  norm_err = flux_par.error()
1083  source.tscalc(False)
1084 
1085  # Fix all parameters in the spectral model
1086  if (self._method != 'NODES' and self._method != 'BINS') or \
1087  (not self['dll_freenodes'].boolean()):
1088  for par in spectral:
1089  par.fix()
1090 
1091  # Re-compute the log-likelihood
1092  like.run()
1093  loglike = like.obs().logL()
1094 
1095  # Store the resulting log-likelihood
1096  norm_scan = []
1097  dloglike = []
1098  ref_norm = norm
1099  if self._method == 'SLICE':
1100  ref_norm = spectral.eval(elogmean)
1101 
1102  # Compute the flux values to evaluate loglike at
1103  log_norm = math.log10(norm)
1104  if (norm_err > 0.0) and (norm > norm_err):
1105  log_step = log_norm - math.log10(norm-norm_err)
1106  log_start = log_norm - (sigsteps/2) * log_step
1107  else:
1108  # For an upper limit bin use a broad range of steps in flux
1109  # from [10^-24, 10^-14] or [norm, 10^-14] whichever is broader
1110  log_start = log_norm
1111  if log_start > -24.0:
1112  log_start = -24.0
1113  log_step = (-14.0 - log_start) / (sigsteps-1)
1114  norm_vals = [10.0 ** (log_start + i*log_step) for i in range(sigsteps)]
1115 
1116  # Loop through normalizations
1117  flux_par.factor_min(0.0)
1118  flux_par.factor_max(1e30)
1119  for new_norm in norm_vals:
1120  flux_par.value(new_norm)
1121 
1122  # Re-run the fit
1123  like.run()
1124 
1125  # Store dlikelihood & norm
1126  dloglike.append(loglike - like.obs().logL())
1127  if self._method == 'SLICE':
1128  norm_scan.append(spectral.eval(elogmean))
1129  else:
1130  norm_scan.append(new_norm)
1131 
1132  # Return
1133  return (norm_scan, dloglike, -loglike)
1134 
1135  def _create_fits(self, results):
1136  """
1137  Create FITS file
1138 
1139  Parameters
1140  ----------
1141  results : list of dict
1142  List of result dictionaries
1143  """
1144  # Create FITS table columns
1145  nrows = self._ebounds.size()
1146  ncols = len(results[0]['norm_scan'])
1147  energy = gammalib.GFitsTableDoubleCol('e_ref', nrows)
1148  energy_low = gammalib.GFitsTableDoubleCol('e_min', nrows)
1149  energy_high = gammalib.GFitsTableDoubleCol('e_max', nrows)
1150  norm = gammalib.GFitsTableDoubleCol('norm', nrows)
1151  norm_err = gammalib.GFitsTableDoubleCol('norm_err', nrows)
1152  norm_ul = gammalib.GFitsTableDoubleCol('norm_ul', nrows)
1153  e2dnde = gammalib.GFitsTableDoubleCol('ref_e2dnde', nrows)
1154  dnde = gammalib.GFitsTableDoubleCol('ref_dnde', nrows)
1155  Npred_values = gammalib.GFitsTableDoubleCol('ref_npred', nrows)
1156  TSvalues = gammalib.GFitsTableDoubleCol('ts', nrows)
1157  loglike = gammalib.GFitsTableDoubleCol('loglike', nrows)
1158  norm_scan = gammalib.GFitsTableDoubleCol('norm_scan', nrows, ncols)
1159  dloglike_scan= gammalib.GFitsTableDoubleCol('dloglike_scan', nrows, ncols)
1160  energy.unit('TeV')
1161  energy_low.unit('TeV')
1162  energy_high.unit('TeV')
1163  norm.unit('')
1164  norm_err.unit('')
1165  norm_ul.unit('')
1166  e2dnde.unit('erg/cm2/s')
1167  dnde.unit('counts/MeV/cm2/s')
1168  Npred_values.unit('counts')
1169  loglike.unit('')
1170  norm_scan.unit('')
1171  dloglike_scan.unit('')
1172 
1173  # File FITS table columns
1174  for i, result in enumerate(results):
1175  energy[i] = result['energy']
1176  energy_low[i] = result['energy_low']
1177  energy_high[i] = result['energy_high']
1178  norm[i] = 1.0
1179  if result['flux'] != 0.0:
1180  norm_err[i] = result['flux_err'] / result['flux']
1181  norm_ul[i] = result['ulimit'] / result['flux']
1182  else:
1183  norm_err[i] = 0.0
1184  norm_ul[i] = 0.0
1185  e2dnde[i] = result['e2dnde']
1186  dnde[i] = result['flux']
1187  Npred_values[i] = result['Npred']
1188  TSvalues[i] = result['TS']
1189  loglike[i] = result['logL']
1190 
1191  # Add likelihood scan values
1192  for fbin in range(ncols):
1193  dloglike_scan[i,fbin] = result['dloglike'][fbin]
1194  if result['flux'] != 0.0:
1195  norm_scan[i,fbin] = result['norm_scan'][fbin] / result['flux']
1196  else:
1197  norm_scan[i,fbin] = 0.0
1198 
1199  # Initialise FITS Table with extension "SPECTRUM"
1200  table = gammalib.GFitsBinTable(nrows)
1201  table.extname('SPECTRUM')
1202 
1203  # Add Header for compatibility with gammalib.GMWLSpectrum
1204  table.card('INSTRUME', 'CTA', 'Name of Instrument')
1205  table.card('TELESCOP', 'CTA', 'Name of Telescope')
1206 
1207  # Append filled columns to fits table
1208  table.append(energy)
1209  table.append(energy_low)
1210  table.append(energy_high)
1211  table.append(norm)
1212  table.append(norm_err)
1213  table.append(norm_ul)
1214  table.append(e2dnde)
1215  table.append(dnde)
1216  table.append(TSvalues)
1217  table.append(Npred_values)
1218 
1219  # Define the SED type
1220  table.card('SED_TYPE', 'norm,e2dnde,dnde,npred', 'SED type')
1221 
1222  # Define the upper limit confidence level
1223  ulimit = ctools.ctulimit()
1224  table.card('UL_CONF', ulimit['confidence'].real(),
1225  'Confidence level of upper limits')
1226 
1227  # Add the likelihood data
1228  if ncols > 0:
1229  table.card('SED_TYPE').value('likelihood,e2dnde,dnde,npred')
1230  table.append(loglike)
1231  table.append(norm_scan)
1232  table.append(dloglike_scan)
1233 
1234  # Stamp table
1235  self._stamp(table)
1236 
1237  # Add metadata keywords
1238  table.card('OBJECT', self['srcname'].string(), 'Source name')
1239  table.card('METHOD', self._method, 'Spectrum generation method')
1240  if self._method == 'BINS':
1241  table.card('BINGAMMA', self['bingamma'].real(), 'Spectral index for BINS method')
1242  table.card('STAT', self['statistic'].string(), 'Optimization statistic')
1243  table.card('EDISP', self['edisp'].boolean(), 'Use energy dispersion?')
1244  table.card('CALCULIM', self['calc_ulim'].boolean(), 'Upper limits computation')
1245  table.card('CALCTS', self['calc_ts'].boolean(), 'Test statistic computation')
1246  table.card('FIXBKG', self['fix_bkg'].boolean(), 'Fix background parameters')
1247  table.card('FIXSRCS', self['fix_srcs'].boolean(), 'Fix other source parameters')
1248 
1249  # Create the FITS file now
1250  self._fits = gammalib.GFits()
1251  self._fits.append(table)
1252 
1253  # Return
1254  return
1255 
1256 
1257  # Public methods
1258  def process(self):
1259  """
1260  Process the script
1261  """
1262  # Get parameters
1263  self._get_parameters()
1264 
1265  # Write input observation container into logger
1266  self._log_observations(gammalib.NORMAL, self.obs(), 'Input observation')
1267 
1268  # Write input models into logger
1269  self._log_models(gammalib.VERBOSE, self.obs().models(), 'Input model')
1270 
1271  # Write spectral binning into logger
1272  self._log_spectral_binning()
1273 
1274  # Adjust model parameters dependent on input user parameters
1275  self._adjust_model_pars()
1276 
1277  # Case A: "SLICE" method
1278  if self._method == 'SLICE':
1279 
1280  # Fit energy bins
1281  results = self._fit_energy_bins()
1282 
1283  # Case B: "NODES" method
1284  elif self._method == 'NODES':
1285 
1286  # Replace source spectrum by nodes function
1288 
1289  # Write replaced models into logger
1290  self._log_models(gammalib.VERBOSE, self.obs().models(),
1291  '"NODES" spectrum generation model')
1292 
1293  # Fit model
1294  results = self._fit_model()
1295 
1296  # Case C: "BINS" method
1297  elif self._method == 'BINS':
1298 
1299  # Replace source spectrum by bins function
1301 
1302  # Write replaced models into logger
1303  self._log_models(gammalib.VERBOSE, self.obs().models(),
1304  '"BINS" spectrum generation model')
1305 
1306  # Fit model
1307  results = self._fit_model()
1308 
1309  # Create FITS file
1310  self._create_fits(results)
1311 
1312  # Optionally publish spectrum
1313  if self['publish'].boolean():
1314  self.publish()
1315 
1316  # Return
1317  return
1318 
1319  def save(self):
1320  """
1321  Save spectrum
1322  """
1323  # Write header
1324  self._log_header1(gammalib.TERSE, 'Save spectrum')
1325 
1326  # Continue only if FITS file is valid
1327  if self._fits != None:
1328 
1329  # Get outmap parameter
1330  outfile = self['outfile'].filename()
1331 
1332  # Log file name
1333  self._log_value(gammalib.NORMAL, 'Spectrum file', outfile.url())
1334 
1335  # Save spectrum
1336  self._fits.saveto(outfile, self['clobber'].boolean())
1337 
1338  # Return
1339  return
1340 
1341  def publish(self, name=''):
1342  """
1343  Publish spectrum
1344 
1345  Parameters
1346  ----------
1347  name : str, optional
1348  Name of spectrum
1349  """
1350  # Write header
1351  self._log_header1(gammalib.TERSE, 'Publish spectrum')
1352 
1353  # Continue only if FITS file is valid
1354  if self._fits != None:
1355 
1356  # Continue only if spectrum is valid
1357  if self._fits.contains('SPECTRUM'):
1358 
1359  # Set default name is user name is empty
1360  if not name:
1361  user_name = self._name()
1362  else:
1363  user_name = name
1364 
1365  # Log file name
1366  self._log_value(gammalib.NORMAL, 'Spectrum name', user_name)
1367 
1368  # Publish spectrum
1369  self._fits.publish('SPECTRUM', user_name)
1370 
1371  # Return
1372  return
1373 
1374  def spectrum(self):
1375  """
1376  Return spectrum FITS file
1377 
1378  Returns:
1379  FITS file containing spectrum
1380  """
1381  # Return
1382  return self._fits
1383 
1384  def models(self, models):
1385  """
1386  Set model
1387  """
1388  # Copy models
1389  self.obs().models(models.clone())
1390 
1391  # Return
1392  return
1393 
1394 
1395 # ======================== #
1396 # Main routine entry point #
1397 # ======================== #
1398 if __name__ == '__main__':
1399 
1400  # Create instance of application
1401  app = csspec(sys.argv)
1402 
1403  # Execute application
1404  app.execute()
def _set_replace_src_spectrum_by_bins
Definition: csspec.py:474
def _set_replace_src_spectrum_by_nodes
Definition: csspec.py:417