ctools  2.0.0
 All Classes Namespaces Files Functions Variables Macros Pages
cslightcrv.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 # ==========================================================================
3 # Generates a lightcurve.
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 gammalib
23 import ctools
24 import cscripts
25 from cscripts import obsutils
26 from cscripts import ioutils
27 from cscripts import mputils
28 
29 
30 # ================ #
31 # cslightcrv class #
32 # ================ #
33 class cslightcrv(ctools.csobservation):
34  """
35  Generates a lightcurve
36 
37  The cslightcrv class generates a light curve for Imaging Air Cherenkov
38  Telescope event data by performing a maximum likelihood fit using
39  ctlike in a series of time bins. The time bins can be either
40  specified in an ASCII file, as an interval divided into equally
41  sized time bins, or can be taken from the Good Time Intervals of the
42  observation(s).
43 
44  The format of the ASCII file is one row per time bin, each specifying
45  the start of stop value of the bin, separated by a whitespace. The
46  times are given in Modified Julian Days (MJD).
47 
48  Examples:
49  >>> lcrv = cslightcrv()
50  >>> lcrv.run()
51  >>> ... (querying for parameters) ...
52  >>> fits = lcrv.lightcurve()
53  Generates a light curve and retrieves the results in
54  a FITS file.
55 
56  >>> lcrv = cslightcrv()
57  >>> lcrv.execute()
58  >>> ... (querying for parameters) ...
59  Generates a light curve and saves results in a FITS file.
60 
61  >>> lcrv = cslightcrv(obs)
62  >>> lcrv.execute()
63  >>> ... (querying for parameters) ...
64  Generates a light curve from the observations in an
65  observation container and saves results in a FITS file.
66  """
67 
68  # Constructor
69  def __init__(self, *argv):
70  """
71  Constructor
72  """
73  # Initialise application by calling the appropriate class constructor
74  self._init_csobservation(self.__class__.__name__, ctools.__version__, argv)
75 
76  # Initialise some members
77  self._srcname = ''
78  self._tbins = gammalib.GGti()
79  self._onoff = False
80  self._stacked = False
81  self._fits = gammalib.GFits()
82  self._nthreads = 0
83  self._excl_reg_map = None # Exclusion region map for on/off analysis
84 
85  # Return
86  return
87 
88  # State methods por pickling
89  def __getstate__(self):
90  """
91  Extend ctools.csobservation getstate method to include some members
92 
93  Returns
94  -------
95  state : dict
96  Pickled instance
97  """
98  # Set pickled dictionary
99  state = {'base' : ctools.csobservation.__getstate__(self),
100  'srcname' : self._srcname,
101  'tbins' : self._tbins,
102  'stacked' : self._stacked,
103  'onoff' : self._onoff,
104  'fits' : self._fits,
105  'nthreads' : self._nthreads,
106  'excl_reg_map' : self._excl_reg_map}
107 
108  # Return pickled dictionary
109  return state
110 
111  def __setstate__(self, state):
112  """
113  Extend ctools.csobservation setstate method to include some members
114 
115  Parameters
116  ----------
117  state : dict
118  Pickled instance
119  """
120  ctools.csobservation.__setstate__(self, state['base'])
121  self._srcname = state['srcname']
122  self._tbins = state['tbins']
123  self._stacked = state['stacked']
124  self._onoff = state['onoff']
125  self._fits = state['fits']
126  self._nthreads = state['nthreads']
127  self._excl_reg_map = state['excl_reg_map']
128 
129  # Return
130  return
131 
132  # Private methods
133  def _get_parameters(self):
134  """
135  Get parameters from parfile
136  """
137  # Setup observations (require response and allow event list, don't
138  # allow counts cube)
139  self._setup_observations(self.obs(), True, True, False)
140 
141  # Set observation statistic
142  self._set_obs_statistic(gammalib.toupper(self['statistic'].string()))
143 
144  # Set models if there are none in the container
145  if self.obs().models().size() == 0:
146  self.obs().models(self['inmodel'].filename())
147 
148  # Get time boundaries
149  self._tbins = self._create_tbounds()
150 
151  # Set On/Off analysis flag and query relevant user parameters
152  self._onoff = self._is_onoff()
153 
154  # Get source name
155  self._srcname = self['srcname'].string()
156 
157  # Set stacked analysis flag and query relevant user parameters
158  if not self._onoff:
159  self._stacked = self._is_stacked()
160 
161  # Query the hidden parameters, just in case
162  self['edisp'].boolean()
163  self['calc_ts'].boolean()
164  self['calc_ulim'].boolean()
165  self['confidence'].real()
166  self['fix_bkg'].boolean()
167  self['fix_srcs'].boolean()
168 
169  # Read ahead output parameters
170  if self._read_ahead():
171  self['outfile'].query()
172 
173  # Write input parameters into logger
174  self._log_parameters(gammalib.TERSE)
175 
176  # Set number of processes for multiprocessing
177  self._nthreads = mputils.nthreads(self)
178 
179  # Return
180  return
181 
182  def _create_tbounds(self):
183  """
184  Creates light curve time bins
185 
186  Returns
187  -------
188  gti : `~gammalib.GGti`
189  Light curve bins in form of Good Time Intervals
190  """
191  # Get MET time reference
192  tref = gammalib.GTimeReference(self['mjdref'].real(),'s','TT','LOCAL')
193 
194  # Initialise Good Time Intervals
195  gti = gammalib.GGti(tref)
196 
197  # Get algorithm to use for defining the time intervals. Possible
198  # values are "FILE", "LIN" and "GTI". This is enforced at
199  # parameter file level, hence no checking is needed.
200  algorithm = self['tbinalg'].string()
201 
202  # If the algorithm is "FILE" then handle a FITS or an ASCII file for
203  # the time bin definition
204  if algorithm == 'FILE':
205 
206  # Get the filename
207  filename = self['tbinfile'].filename()
208 
209  # If the file a FITS file then load GTIs directly
210  if filename.is_fits():
211  gti = gammalib.GGti(filename)
212 
213  # ... otherwise load file the ASCII file as CSV file and construct
214  # the GTIs from the rows of the CSV file. It is expected that the
215  # CSV file has two columns containing the "START" and "STOP"
216  # values of the time bins. No header row is expected.
217  else:
218  csv = gammalib.GCsv(filename)
219  for i in range(csv.nrows()):
220  tmin = gammalib.GTime()
221  tmax = gammalib.GTime()
222  tmin.mjd(csv.real(i,0))
223  tmax.mjd(csv.real(i,1))
224  gti.append(tmin,tmax)
225 
226  # If the algorithm is "LIN" then use a linear time binning, defined by
227  # the "tmin", "tmax" and "tbins" user parameters
228  elif algorithm == 'LIN':
229 
230  # Get start and stop time and number of time bins
231  time_min = self['tmin'].time(tref)
232  time_max = self['tmax'].time(tref)
233  nbins = self['tbins'].integer()
234 
235  # Compute time step in seconds and setup the GTIs
236  time_step = (time_max - time_min) / float(nbins)
237  for i in range(nbins):
238  tmin = time_min + i *time_step
239  tmax = time_min + (i+1)*time_step
240  gti.append(tmin,tmax)
241 
242  # If the algorithm is "GTI" then extract the GTIs from the observations
243  # in the container and use them for the light curve time binning
244  elif algorithm == 'GTI':
245 
246  # Append the GTIs of all observations
247  for obs in self.obs():
248  for i in range(obs.events().gti().size()):
249  gti.append(obs.events().gti().tstart(i),
250  obs.events().gti().tstop(i))
251 
252  # Return Good Time Intervals
253  return gti
254 
256  """
257  Return list of free parameter names
258 
259  Returns
260  -------
261  names : list of str
262  List of free parameter names.
263  """
264  # Initialise empty list of free parameter names
265  names = []
266 
267  # Collect list of free parameter names
268  for par in self.obs().models()[self._srcname]:
269  if par.is_free():
270  names.append(par.name())
271 
272  # Return names
273  return names
274 
276  """
277  Adjust model parameters dependent on user parameters
278  """
279  # Write header
280  self._log_header1(gammalib.TERSE, 'Adjust model parameters')
281 
282  # Adjust model parameters dependent on input user parameters
283  for model in self.obs().models():
284 
285  # Set TS flag for all models to false. The source of interest will
286  # be set to true later
287  model.tscalc(False)
288 
289  # Get the model classname to distinguish later sky from background
290  # models
291  classname = model.classname()
292 
293  # Log model name
294  self._log_header3(gammalib.NORMAL, model.name())
295 
296  # If the model is the source of interest and the 'calc_ts' parameter
297  # is true then enable the TS computation for the source
298  if model.name() == self._srcname:
299  if self['calc_ts'].boolean():
300  model.tscalc(True)
301 
302  # ... otherwise, if the model is not a sky model and the 'fix_bkg'
303  # parameter is true or if the model is a sky model and the
304  # 'fix_srcs' parameter is true then fix all parameters of the model
305  elif ((self['fix_bkg'].boolean() and classname != 'GModelSky') or
306  (self['fix_srcs'].boolean() and classname == 'GModelSky')):
307  for par in model:
308  if par.is_free():
309  par.fix()
310  self._log_value(gammalib.NORMAL, par.name(), 'fixed')
311 
312  # Return
313  return
314 
315  def _create_fits_table(self, results):
316  """
317  Creates FITS binary table containing light curve results
318 
319  Parameters
320  ----------
321  results : list of dict
322  List of result dictionaries
323 
324  Returns
325  -------
326  table : `~gammalib.GFitsBinTable`
327  FITS binary table containing light curve
328  """
329  # Determine number of rows in FITS table
330  nrows = len(results)
331 
332  # Create FITS Table with extension "LIGHTCURVE"
333  table = gammalib.GFitsBinTable(nrows)
334  table.extname('LIGHTCURVE')
335 
336  # Append time columns
337  ioutils.append_result_column(table, results, 'MJD', 'days', 'mjd')
338  ioutils.append_result_column(table, results, 'e_MJD', 'days', 'e_mjd')
339 
340  # Append parameter columns
341  ioutils.append_model_par_column(table,
342  self.obs().models()[self._srcname],
343  results)
344 
345  # Append Test Statistic column "TS"
346  ioutils.append_result_column(table, results, 'TS', '', 'ts')
347 
348  # Append upper limit columns
349  ioutils.append_result_column(table, results, 'DiffUpperLimit',
350  'ph/cm2/s/MeV', 'ul_diff')
351  ioutils.append_result_column(table, results, 'FluxUpperLimit',
352  'ph/cm2/s', 'ul_flux')
353  ioutils.append_result_column(table, results, 'EFluxUpperLimit',
354  'erg/cm2/s', 'ul_eflux')
355 
356  # Return table
357  return table
358 
359  def _compute_ulimit(self, obs):
360  """
361  Computes upper limit
362 
363  Parameters
364  ----------
365  obs : `~gammalib.GObservations`
366  Observation container
367 
368  Returns
369  -------
370  ul_diff, ul_flux, ul_eflux : tuple of float
371  Upper limits, -1.0 of not computed
372  """
373  # Initialise upper flux limit
374  ul_diff = -1.0
375  ul_flux = -1.0
376  ul_eflux = -1.0
377 
378  # Perform computation only if requested
379  if self['calc_ulim'].boolean():
380 
381  # Write header in logger
382  self._log_header3(gammalib.EXPLICIT, 'Computing upper limit')
383 
384  # Create copy of observations
385  cpy_obs = obs.copy()
386 
387  # Fix parameters of source of interest in copy of observations.
388  # This assures that the original spectral parameters and position
389  # are used in the upper limit search. The ctulimit tool makes sure
390  # that the flux parameter is freed when needed.
391  source = cpy_obs.models()[self._srcname]
392  for par in source:
393  if par.is_free():
394  par.fix()
395 
396  # Create instance of upper limit tool
397  ulimit = ctools.ctulimit(cpy_obs)
398  ulimit['srcname'] = self._srcname
399  ulimit['eref'] = 1.0
400  ulimit['emin'] = self['emin'].real()
401  ulimit['emax'] = self['emax'].real()
402  ulimit['sigma_min'] = 0.0
403  ulimit['sigma_max'] = 3.0
404  ulimit['confidence'] = self['confidence'].real()
405 
406  # Try to run upper limit tool and catch any exceptions
407  try:
408  ulimit.run()
409  ul_diff = ulimit.diff_ulimit()
410  ul_flux = ulimit.flux_ulimit()
411  ul_eflux = ulimit.eflux_ulimit()
412  except:
413  self._log_string(gammalib.TERSE, 'Upper limit calculation failed.')
414  ul_diff = -1.0
415  ul_flux = -1.0
416  ul_eflux = -1.0
417 
418  # Return upper limit tuple
419  return ul_diff, ul_flux, ul_eflux
420 
421  def _timebin(self, i):
422  """
423  Run likelihood analysis in one time bin
424 
425  Parameters
426  ----------
427  i : int
428  time bin number
429 
430  Returns
431  -------
432  result : dict
433  Results of the likelihood analysis
434  """
435  # Get names of free parameters
436  pars = self._get_free_par_names()
437 
438  # Get time boundaries
439  tmin = self._tbins.tstart(i)
440  tmax = self._tbins.tstop(i)
441 
442  # Write time bin into header
443  self._log_header2(gammalib.TERSE, 'MJD %f - %f ' %
444  (tmin.mjd(), tmax.mjd()))
445 
446  # Compute time bin center and time width
447  twidth = 0.5 * (tmax - tmin) # in seconds
448  tmean = tmin + twidth
449 
450  # Initialise result dictionary
451  result = {'mjd': tmean.mjd(),
452  'e_mjd': twidth / gammalib.sec_in_day,
453  'ts': 0.0,
454  'ul_diff': 0.0,
455  'ul_flux': 0.0,
456  'ul_eflux': 0.0,
457  'pars': pars,
458  'values': {}}
459 
460  # Log information
461  self._log_header3(gammalib.EXPLICIT, 'Selecting observations')
462 
463  # Select observations
464  select = cscripts.csobsselect(self.obs())
465  select['pntselect'] = 'CIRCLE'
466  select['coordsys'] = 'GAL'
467  select['glon'] = 0.0
468  select['glat'] = 0.0
469  select['rad'] = 180.0
470  select['tmin'] = tmin
471  select['tmax'] = tmax
472  select.run()
473 
474  # Retrieve observations
475  obs = select.obs()
476 
477  # If there are observations then select now events from them
478  if obs.size() > 0:
479 
480  # Log information
481  self._log_header3(gammalib.EXPLICIT, 'Selecting events')
482 
483  # Select events
484  select = ctools.ctselect(obs)
485  select['emin'] = self['emin'].real()
486  select['emax'] = self['emax'].real()
487  select['tmin'] = tmin
488  select['tmax'] = tmax
489  select['rad'] = 'UNDEFINED'
490  select['ra'] = 'UNDEFINED'
491  select['dec'] = 'UNDEFINED'
492  select.run()
493 
494  # Retrieve observations
495  obs = select.obs()
496 
497  # Continue only if there are observations
498  if obs.size() > 0:
499 
500  # Deal with stacked and On/Off Observations
501  if self._stacked or self._onoff:
502 
503  # If a stacked analysis is requested bin the events
504  # and compute the stacked response functions and setup
505  # an observation container with a single stacked observation.
506  if self._stacked:
507  new_obs = obsutils.get_stacked_obs(self, obs, nthreads=1)
508 
509  # ... otherwise if On/Off analysis is requested generate
510  # the On/Off observations and response
511  elif self._onoff:
512  new_obs = obsutils.get_onoff_obs(self, obs, nthreads=1)
513 
514  # Extract models
515  models = new_obs.models()
516 
517  # Fix background models if required
518  if self['fix_bkg'].boolean():
519  for model in models:
520  if model.classname() != 'GModelSky':
521  for par in model:
522  par.fix()
523 
524  # Put back models
525  new_obs.models(models)
526 
527  # Continue with new oberservation container
528  obs = new_obs
529 
530  # Header
531  self._log_header3(gammalib.EXPLICIT, 'Fitting the data')
532 
533  # Do maximum likelihood model fitting
534  like = ctools.ctlike(obs)
535  like['edisp'] = self['edisp'].boolean()
536  like['nthreads'] = 1 # Avoids OpenMP conflict
537  like.run()
538 
539  # Skip bin if no event was present
540  if like.obs().logL() == 0.0:
541 
542  # Signal skipping of bin
543  self._log_value(gammalib.TERSE, 'Warning',
544  'No event in this time bin, skip bin.')
545 
546  # Set all results to 0
547  for par in pars:
548  result['values'][par] = 0.0
549  result['values']['e_'+par] = 0.0
550 
551  # Otherwise fill in results dictionary
552  else:
553  # Retrieve model fitting results for source of interest
554  source = like.obs().models()[self._srcname]
555 
556  # Extract parameter values
557  for par in pars:
558  result['values'][par] = source[par].value()
559  result['values']['e_'+par] = source[par].error()
560 
561  # Calculate upper limit (-1 if not computed)
562  ul_diff, ul_flux, ul_eflux = self._compute_ulimit(obs)
563  if ul_diff > 0.0:
564  result['ul_diff'] = ul_diff
565  result['ul_flux'] = ul_flux
566  result['ul_eflux'] = ul_eflux
567 
568  # Extract Test Statistic value
569  if self['calc_ts'].boolean():
570  result['ts'] = source.ts()
571 
572  # Log results for this time bin
573  self._log.header3('Results')
574  pars = self._get_free_par_names()
575  for par in pars:
576  value = source[par].value()
577  error = source[par].error()
578  unit = source[par].unit()
579  self._log_value(gammalib.NORMAL, par,
580  str(value)+' +/- '+str(error)+' '+unit)
581  if ul_diff > 0.0:
582  self._log_value(gammalib.NORMAL, 'Upper flux limit',
583  str(result['ul_diff'])+' ph/cm2/s/MeV')
584  self._log_value(gammalib.NORMAL, 'Upper flux limit',
585  str(result['ul_flux'])+' ph/cm2/s')
586  self._log_value(gammalib.NORMAL, 'Upper flux limit',
587  str(result['ul_eflux'])+' erg/cm2/s')
588  if self['calc_ts'].boolean():
589  self._log_value(gammalib.NORMAL, 'Test Statistic', result['ts'])
590 
591  # Otherwise, if observations size is 0, signal bin is skipped and
592  # fill results table with zeros
593  else:
594  self._log_value(gammalib.TERSE, 'Warning',
595  'No observations available in this time bin, '
596  'skip bin.')
597 
598  # Set all results to 0
599  for par in pars:
600  result['values'][par] = 0.0
601  result['values']['e_' + par] = 0.0
602 
603  # Return result dictionary
604  return result
605 
606 
607  # Public methods
608  def process(self):
609  """
610  Process the script
611  """
612  # Get parameters
613  self._get_parameters()
614 
615  # Write observation into logger
616  self._log_observations(gammalib.NORMAL, self.obs(), 'Observation')
617 
618  # Get time boundaries
619  tmin = self._tbins.tstart(0)
620  tmax = self._tbins.tstop(self._tbins.size()-1)
621 
622  # Select events
623  select = ctools.ctselect(self.obs())
624  select['emin'] = self['emin'].real()
625  select['emax'] = self['emax'].real()
626  select['tmin'] = tmin
627  select['tmax'] = tmax
628  select['rad'] = 'UNDEFINED'
629  select['ra'] = 'UNDEFINED'
630  select['dec'] = 'UNDEFINED'
631  select.run()
632 
633  # Extract observations
634  self.obs(select.obs().copy())
635 
636  # Write observation into logger
637  self._log_header1(gammalib.TERSE,
638  gammalib.number('Selected observation',
639  len(self.obs())))
640 
641  # Adjust model parameters dependent on user parameters
642  self._adjust_model_pars()
643 
644  # Write header
645  self._log_header1(gammalib.TERSE, 'Generate lightcurve')
646 
647  # If more than a single thread is requested then use multiprocessing
648  if self._nthreads > 1:
649 
650  # Compute time bins
651  args = [(self, '_timebin', i)
652  for i in range(self._tbins.size())]
653  poolresults = mputils.process(self._nthreads, mputils.mpfunc, args)
654 
655  # Construct results
656  results = []
657  for i in range(self._tbins.size()):
658  results.append(poolresults[i][0])
659  self._log_string(gammalib.TERSE, poolresults[i][1]['log'], False)
660 
661  # Otherwise loop over time bins and run time bin analysis
662  else:
663  results = []
664  for i in range(self._tbins.size()):
665  results.append(self._timebin(i))
666 
667  # Create FITS table from results
668  table = self._create_fits_table(results)
669 
670  # Create FITS file and append FITS table to FITS file
671  self._fits = gammalib.GFits()
672  self._fits.append(table)
673 
674  # Stamp FITS file
675  self._stamp(self._fits)
676 
677  # Optionally publish light curve
678  if self['publish'].boolean():
679  self.publish()
680 
681  # Return
682  return
683 
684  def save(self):
685  """
686  Save light curve
687  """
688  # Write header
689  self._log_header1(gammalib.TERSE, 'Save light curve')
690 
691  # Continue only if filename is valid
692  if self['outfile'].is_valid():
693 
694  # Get light curve filename
695  outfile = self['outfile'].filename()
696 
697  # Log file name
698  self._log_value(gammalib.NORMAL, 'Light curve file', outfile.url())
699 
700  # Save spectrum
701  self._fits.saveto(outfile, self._clobber())
702 
703  # Return
704  return
705 
706  def publish(self, name=''):
707  """
708  Publish light curve
709 
710  Parameters
711  ----------
712  name : str, optional
713  Name of light curve
714  """
715  # Write header
716  self._log_header1(gammalib.TERSE, 'Publish light curve')
717 
718  # Continue only if light curve is valid
719  if self._fits.contains('LIGHTCURVE'):
720 
721  # Set default name is user name is empty
722  if not name:
723  user_name = self._name()
724  else:
725  user_name = name
726 
727  # Log file name
728  self._log_value(gammalib.NORMAL, 'Light curve name', user_name)
729 
730  # Publish light curve
731  self._fits.publish('LIGHTCURVE', user_name)
732 
733  # Return
734  return
735 
736  def lightcurve(self):
737  """
738  Return light curve FITS file
739 
740  Returns
741  -------
742  fits : `~gammalib.GFits()`
743  FITS file containing light curve
744  """
745  # Return
746  return self._fits
747 
748  def models(self, models):
749  """
750  Set model
751 
752  Parameters
753  ----------
754  models : `~gammalib.GModels()`
755  Set model container
756  """
757  # Copy models
758  self.obs().models(models)
759 
760  # Return
761  return
762 
763  def exclusion_map(self, object=None):
764  """
765  Return and optionally set the exclusion regions map
766 
767  Parameters
768  ----------
769  object : `~gammalib.GSkyRegion` or `~gammalib.GSkyMap` or `~gammalib.GFilename`
770  Exclusion regions object
771 
772  Returns
773  -------
774  region : `~gammalib.GSkyRegionMap`
775  Exclusion regions map
776  """
777  # If a regions object is provided then set the regions ...
778  if object is not None:
779  self._excl_reg_map = gammalib.GSkyRegionMap(object)
780 
781  # Return
782  return self._excl_reg_map
783 
784 
785 # ======================== #
786 # Main routine entry point #
787 # ======================== #
788 if __name__ == '__main__':
789 
790  # Create instance of application
791  app = cslightcrv(sys.argv)
792 
793  # Execute application
794  app.execute()