ctools 2.1.0.dev
Loading...
Searching...
No Matches
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# ==========================================================================
21import sys
22import gammalib
23import ctools
24import cscripts
25from cscripts import obsutils
26from cscripts import ioutils
27from cscripts import mputils
28
29
30# ================ #
31# cslightcrv class #
32# ================ #
33class 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
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
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# ======================== #
788if __name__ == '__main__':
789
790 # Create instance of application
791 app = cslightcrv(sys.argv)
792
793 # Execute application
794 app.execute()
_create_fits_table(self, results)
exclusion_map(self, object=None)