ctools 2.1.0.dev
Loading...
Searching...
No Matches
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# ==========================================================================
21import sys
22import gammalib
23import ctools
24from cscripts import obsutils
25from cscripts import ioutils
26from cscripts import mputils
27
28
29# ================ #
30# csphasecrv class #
31# ================ #
32class 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
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
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# ======================== #
583if __name__ == '__main__':
584
585 # Create instance of application
586 app = csphasecrv(sys.argv)
587
588 # Execute application
589 app.execute()
_create_fits_table(self, results)
exclusion_map(self, object=None)