ctools  2.0.0
 All Classes Namespaces Files Functions Variables Macros Pages
csphasecrv.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 # ==========================================================================
3 # Generates spectra as a function of phase
4 #
5 # Copyright (C) 2017-2022 Rolf Buehler
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 from cscripts import obsutils
25 from cscripts import ioutils
26 from cscripts import mputils
27 
28 
29 # ================ #
30 # csphasecrv class #
31 # ================ #
32 class csphasecrv(ctools.csobservation):
33  """
34  Generates spectra in phase bins
35 
36  This script computes spectra by performing a maximum likelihood fit
37  using :doc:`ctlike` in a series of phase bins for pulsars. The phase bins
38  can be either specified in an ASCII file or as an interval divided into
39  equally sized phase bins.
40 
41  Examples:
42  >>> phcrv = csphasecrv()
43  >>> phcrv.run()
44  >>> ... (querying for parameters) ...
45  >>> phcrv = phrv.phasecrv()
46  Generates phase fits and retrieves dictionary with best fit models.
47 
48  >>> phcrv = csphasecrv(obs)
49  >>> phcrv.execute()
50  >>> ... (querying for parameters) ...
51  Generates phase fits from the observations
52  and saves results in XML files.
53  """
54 
55  # Constructor
56  def __init__(self, *argv):
57  """
58  Constructor
59  """
60  # Initialise application by calling the appropriate class constructor
61  self._init_csobservation(self.__class__.__name__, ctools.__version__, argv)
62 
63  # Initialise some members. Phases are stored in a nested list
64  # [[ph1min,ph1max], [ph2min,ph2max],..]
65  self._srcname = ''
66  self._phbins = [[0.0,1.0]]
67  self._onoff = False
68  self._stacked = False
69  self._fits = gammalib.GFits()
70  self._fitmodels = {}
71  self._nthreads = 0
72  self._excl_reg_map = None # Exclusion region map for on/off analysis
73 
74  # Return
75  return
76 
77  # State methods por pickling
78  def __getstate__(self):
79  """
80  Extend ctools.csobservation getstate method to include some members
81 
82  Returns
83  -------
84  state : dict
85  Pickled instance
86  """
87  # Set pickled dictionary
88  state = {'base' : ctools.csobservation.__getstate__(self),
89  'srcname' : self._srcname,
90  'phbins' : self._phbins,
91  'onoff' : self._onoff,
92  'stacked' : self._stacked,
93  'fits' : self._fits,
94  'fitmodels' : self._fitmodels,
95  'nthreads' : self._nthreads,
96  'excl_reg_map' : self._excl_reg_map}
97 
98  # Return pickled dictionary
99  return state
100 
101  def __setstate__(self, state):
102  """
103  Extend ctools.csobservation setstate method to include some members
104 
105  Parameters
106  ----------
107  state : dict
108  Pickled instance
109  """
110  ctools.csobservation.__setstate__(self, state['base'])
111  self._srcname = state['srcname']
112  self._phbins = state['phbins']
113  self._onoff = state['onoff']
114  self._stacked = state['stacked']
115  self._fits = state['fits']
116  self._fitmodels = state['fitmodels']
117  self._nthreads = state['nthreads']
118  self._excl_reg_map = state['excl_reg_map']
119 
120  # Return
121  return
122 
123  # Private methods
124  def _get_parameters(self):
125  """
126  Get parameters from parfile
127  """
128  # Setup observations (require response and allow event list, don't
129  # allow counts cube)
130  self._setup_observations(self.obs(), True, True, False)
131 
132  # Set observation statistic
133  self._set_obs_statistic(gammalib.toupper(self['statistic'].string()))
134 
135  # Set models if there are none in the container
136  if self.obs().models().size() == 0:
137  self.obs().models(self['inmodel'].filename())
138 
139  # Get phase boundaries
140  self._phbins = self._create_tbounds()
141 
142  # Set On/Off analysis flag and query relevant user parameters
143  self._onoff = self._is_onoff()
144 
145  # Get source name
146  self._srcname = self['srcname'].string()
147 
148  # If cube analysis is selected
149  # Set stacked analysis flag and query relevant user parameters
150  if not self._onoff:
151  self._stacked = self._is_stacked()
152 
153  # Query the hidden parameters, just in case
154  self['edisp'].boolean()
155 
156  # Read ahead output parameters
157  if self._read_ahead():
158  self['outfile'].filename()
159 
160  # Write input parameters into logger
161  self._log_parameters(gammalib.TERSE)
162 
163  # Set number of processes for multiprocessing
164  self._nthreads = mputils.nthreads(self)
165 
166  # Return
167  return
168 
169  def _create_tbounds(self):
170  """
171  Creates phase bins
172 
173  Returns
174  -------
175  phbins : `[]`
176  List of phase bins
177  """
178  # Initialise Good Time Intervals
179  phbins = []
180 
181  # Get algorithm to use for defining the time intervals. Possible
182  # values are "FILE" or "LIN". This is enforced at
183  # parameter file level, hence no checking is needed.
184  algorithm = self['phbinalg'].string()
185 
186  # If the algorithm is "FILE" then handle a FITS or an ASCII file for
187  # the time bin definition
188  if algorithm == 'FILE':
189 
190  # Get the filename
191  filename = self['phbinfile'].filename()
192 
193  # Load ASCII file as CSV file and construct
194  # the GTIs from the rows of the CSV file. It is expected that the
195  # CSV file has two columns containing the "START" and "STOP"
196  # values of the phase bins. No header row is expected.
197  csv = gammalib.GCsv(filename)
198  for i in range(csv.nrows()):
199  phmin = csv.real(i,0)
200  phmax = csv.real(i,1)
201  phbins.append([phmin,phmax])
202 
203  # If the algorithm is "LIN" then use a linear time binning, defined by
204  # the "phbins" user parameters
205  elif algorithm == 'LIN':
206 
207  # Get start and stop time and number of time bins
208  nbins = self['phbins'].integer()
209 
210  # Compute time step and setup the GTIs
211  ph_step = 1.0 / float(nbins)
212  for i in range(nbins):
213  phmin = i * ph_step
214  phmax = (i+1) * ph_step
215  phbins.append([phmin,phmax])
216 
217  # Return Good Time Intervals
218  return phbins
219 
221  """
222  Return list of free parameter names
223 
224  Returns
225  -------
226  names : list of str
227  List of free parameter names.
228  """
229  # Initialise empty list of free parameter names
230  names = []
231 
232  # Collect list of free parameter names
233  for par in self.obs().models()[self._srcname]:
234  if par.is_free():
235  names.append(par.name())
236 
237  # Return names
238  return names
239 
240  def _create_fits_table(self, results):
241  """
242  Creates FITS binary table containing phase curve results
243 
244  Parameters
245  ----------
246  results : list of dict
247  List of result dictionaries
248 
249  Returns
250  -------
251  table : `~gammalib.GFitsBinTable`
252  FITS binary table containing phase curve
253  """
254  # Determine number of rows in FITS table
255  nrows = len(results)
256 
257  # Create FITS Table with extension "PHASECURVE"
258  table = gammalib.GFitsBinTable(nrows)
259  table.extname('PHASECURVE')
260 
261  # Append phase columns
262  ioutils.append_result_column(table, results, 'PHASE_MIN', '', 'phmin')
263  ioutils.append_result_column(table, results, 'PHASE_MAX', '', 'phmax')
264 
265  # Append parameter columns
266  ioutils.append_model_par_column(table, self.obs().models()[self._srcname],
267  results)
268 
269  # Return table
270  return table
271 
272  def _create_fits(self):
273  """
274  Create FITS file object from fit results
275  """
276  # Initialise list of result dictionaries
277  results = []
278 
279  # Get source parameters
280  pars = self._get_free_par_names()
281 
282  # Loop over time bins
283  for i in range(len(self._phbins)):
284 
285  # Get time boundaries
286  phmin = self._phbins[i][0]
287  phmax = self._phbins[i][1]
288 
289  # Initialise result dictionary
290  result = {'phmin': phmin,
291  'phmax': phmax,
292  'pars': pars,
293  'values': {}}
294 
295  # Store fit results
296  phname = str(phmin)+'-'+str(phmax)
297 
298  # If the model contains the source of interest fill results
299  try:
300  source = self._fitmodels[phname][self._srcname]
301  for par in pars:
302  result['values'][par] = source[par].value()
303  result['values']['e_'+par] = source[par].error()
304 
305  # ... otherwise fills with zeros
306  except:
307  for par in pars:
308  result['values'][par] = 0.
309  result['values']['e_'+par] = 0.
310 
311  # Append result to list of dictionaries
312  results.append(result)
313 
314  # Create FITS table from results
315  table = self._create_fits_table(results)
316 
317  # Create FITS file and append FITS table to FITS file
318  self._fits = gammalib.GFits()
319  self._fits.append(table)
320 
321  # Stamp FITS file
322  self._stamp(self._fits)
323 
324  # Return
325  return
326 
327  def _save_fits(self):
328  """
329  Saved phase fit results into a fits file
330  """
331  # Write header
332  self._log_header1(gammalib.TERSE, 'Save phase FITS file')
333 
334  # Create FITS file
335  self._create_fits()
336 
337  # Get file name
338  outfile = self['outfile'].filename()
339 
340  # Log file name
341  self._log_value(gammalib.NORMAL, 'Phase curve file', outfile.url())
342 
343  # Save to fits
344  self._fits.saveto(outfile, self._clobber())
345 
346  # Return
347  return
348 
349  def _save_xml(self):
350  """
351  Save phase fit results into one XML model each bin
352  """
353  # Write header
354  self._log_header1(gammalib.TERSE, 'Save phase XML files')
355 
356  # Get file name
357  outname = self['outfile'].filename()
358 
359  # Loop over all phase bins
360  for phbin in self._phbins:
361 
362  # Set file name for phase bin
363  phname = str(phbin[0])+'-'+str(phbin[1])
364  replace_name = '_phase_'+phname+'.xml'
365  outname_phase = (outname.file()).replace('.fits', replace_name)
366  outfile = outname.path() + outname_phase
367 
368  # Save XML file
369  self._fitmodels[phname].save(outfile)
370 
371  # Log file name
372  name = 'Phase [%s] file' % phname
373  self._log_value(gammalib.NORMAL, name, outfile)
374 
375  # Return
376  return
377 
378  def _phase_bin(self, phbin):
379  """
380  Analyse one phase bin
381 
382  Parameters
383  ----------
384  phbin : list of float
385  Phase bin limits
386  """
387 
388  # Write time bin into header
389  self._log_header2(gammalib.TERSE, 'PHASE %f - %f' %
390  (phbin[0], phbin[1]))
391 
392  # Select events
393  select = ctools.ctselect(self.obs())
394  select['emin'] = self['emin'].real()
395  select['emax'] = self['emax'].real()
396  select['tmin'] = 'UNDEFINED'
397  select['tmax'] = 'UNDEFINED'
398  select['rad'] = 'UNDEFINED'
399  select['ra'] = 'UNDEFINED'
400  select['dec'] = 'UNDEFINED'
401  select['expr'] = 'PHASE>' + str(phbin[0]) + ' && PHASE<' + str(phbin[1])
402  select.run()
403 
404  # Set phase string
405  phstr = str(phbin[0]) + '-' + str(phbin[1])
406 
407  # Add phase to observation id
408  for i in range(0, select.obs().size()):
409  oldid = select.obs()[i].id()
410  select.obs()[i].id(oldid + '_' + phstr)
411  obs = select.obs()
412 
413  # If an On/Off analysis is requested generate the On/Off observations
414  if self._onoff:
415  obs = obsutils.get_onoff_obs(self, select.obs(), nthreads=1)
416 
417  # ... otherwise, if stacked analysis is requested then bin the
418  # events and compute the stacked response functions and setup
419  # an observation container with a single stacked observation.
420  elif self._stacked:
421  obs = obsutils.get_stacked_obs(self, select.obs())
422 
423  # Header
424  self._log_header3(gammalib.EXPLICIT, 'Fitting the data')
425 
426  # The On/Off analysis can produce empty observation containers,
427  # e.g., when on-axis observations are used. To avoid ctlike asking
428  # for a new observation container (or hang, if in interactive mode)
429  # we'll run ctlike only if the size is >0
430  if obs.size() > 0:
431 
432  # Do maximum likelihood model fitting
433  like = ctools.ctlike(obs)
434  like['edisp'] = self['edisp'].boolean()
435  like['nthreads'] = 1 # Avoids OpenMP conflict
436  like.run()
437 
438  # Renormalize models to phase selection
439  # TODO move the scaling from the temporal to the spectral component
440  for model in like.obs().models():
441  scaled_norm = model['Normalization'].value() / (phbin[1] - phbin[0])
442  model['Normalization'].value(scaled_norm)
443 
444  # Store fit model
445  fitmodels = like.obs().models().copy()
446 
447  # ... otherwise we set an empty model container
448  else:
449  self._log_string(gammalib.TERSE,
450  'PHASE %f - %f: no observations available'
451  ' for fitting' % (phbin[0], phbin[1]))
452 
453  # Set empty models container
454  fitmodels = gammalib.GModels()
455 
456  # Set results
457  result = {'phstr': phstr, 'fitmodels': fitmodels}
458 
459  # Return results
460  return result
461 
462 
463  # Public methods
464  def process(self):
465  """
466  Process the script
467  """
468  # Get parameters
469  self._get_parameters()
470 
471  # Write observation into logger
472  self._log_observations(gammalib.NORMAL, self.obs(), 'Observation')
473 
474  # Dictionary to save phase fitted models
475  self._fitmodels = {}
476 
477  # Write header
478  self._log_header1(gammalib.TERSE, 'Generate phase curve')
479 
480  # If more than a single thread is requested then use multiprocessing
481  if self._nthreads > 1:
482 
483  # Compute phase bins
484  args = [(self, '_phase_bin', phbin) for phbin in self._phbins]
485  poolresults = mputils.process(self._nthreads, mputils.mpfunc, args)
486 
487  # Construct results
488  for i in range(len(self._phbins)):
489  self._fitmodels[poolresults[i][0]['phstr']] = poolresults[i][0]['fitmodels']
490  self._log_string(gammalib.TERSE, poolresults[i][1]['log'], False)
491 
492  # Otherwise, loop over all phases
493  else:
494  for phbin in self._phbins:
495  result = self._phase_bin(phbin)
496  self._fitmodels[result['phstr']] = result['fitmodels']
497 
498  # Create FITS file
499  self._create_fits()
500 
501  # Optionally publish phase curve
502  if self['publish'].boolean():
503  self.publish()
504 
505  # Return
506  return
507 
508  def save(self):
509  """
510  Saves results to fits and XML
511  """
512  # Save results to FITS
513  self._save_fits()
514 
515  # Save results to XML files (one per phase bin)
516  self._save_xml()
517 
518  # Return
519  return
520 
521  def publish(self, name=''):
522  """
523  Publish phase curve
524 
525  Parameters
526  ----------
527  name : str, optional
528  Name of phase curve
529  """
530  # Write header
531  self._log_header1(gammalib.TERSE, 'Publish phase curve')
532 
533  # Continue only if phase curve is valid
534  if self._fits.contains('PHASECURVE'):
535 
536  # Set default name is user name is empty
537  if not name:
538  user_name = self._name()
539  else:
540  user_name = name
541 
542  # Log file name
543  self._log_value(gammalib.NORMAL, 'Phase curve name', user_name)
544 
545  # Publish phase curve
546  self._fits.publish('PHASECURVE', user_name)
547 
548  # Return
549  return
550 
551  def phasecurve(self):
552  """
553  Return dictionary with best fit models
554  """
555  # Return
556  return self._fitmodels
557 
558  def exclusion_map(self, object=None):
559  """
560  Return and optionally set the exclusion regions map
561 
562  Parameters
563  ----------
564  object : `~gammalib.GSkyRegion` or `~gammalib.GSkyMap` or `~gammalib.GFilename`
565  Exclusion regions object
566 
567  Returns
568  -------
569  region : `~gammalib.GSkyRegionMap`
570  Exclusion regions map
571  """
572  # If a regions object is provided then set the regions ...
573  if object is not None:
574  self._excl_reg_map = gammalib.GSkyRegionMap(object)
575 
576  # Return
577  return self._excl_reg_map
578 
579 
580 # ======================== #
581 # Main routine entry point #
582 # ======================== #
583 if __name__ == '__main__':
584 
585  # Create instance of application
586  app = csphasecrv(sys.argv)
587 
588  # Execute application
589  app.execute()