csphagen.py

Knödlseder Jürgen, 02/08/2018 12:13 PM

Download (18.2 KB)

 
1
#! /usr/bin/env python
2
# ==========================================================================
3
# Computes the PHA spectra for source/background and ARF/RMF files using the
4
# reflected region method
5
#
6
# Copyright (C) 2017-2018 Luigi Tibaldo
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
#
21
# ==========================================================================
22
import gammalib
23
import ctools
24
import math
25
import sys
26

    
27

    
28
# =============== #
29
# csfindobs class #
30
# =============== #
31
class csphagen(ctools.csobservation):
32
    """
33
    Generate PHA, ARF and RMF files for classical IACT spectral analysis
34
    """
35

    
36
    # Constructor
37
    def __init__(self, *argv):
38
        """
39
        Constructor
40
        """
41
        # Initialise application by calling the appropriate class constructor
42
        self._init_csobservation(self.__class__.__name__, ctools.__version__, argv)
43

    
44
        # Initialise other variables
45
        self._ebounds       = gammalib.GEbounds()
46
        self._src_dir       = gammalib.GSkyDir()
47
        self._src_reg       = gammalib.GSkyRegions()
48
        self._bkg_regs      = []
49
        self._excl_reg      = None
50
        self._has_exclusion = False
51
        self._srcshape      = ''
52
        self._rad           = 0.0
53

    
54
        # Return
55
        return
56

    
57

    
58
    # Private methods
59
    def _query_src_direction(self):
60
        """
61
        Set up the source direction parameter.
62
        Query relevant parameters.
63
        """
64
        self._src_dir = gammalib.GSkyDir()
65
        coordsys = self['coordsys'].string()
66
        if coordsys == 'CEL':
67
            ra  = self['ra'].real()
68
            dec = self['dec'].real()
69
            self._src_dir.radec_deg(ra, dec)
70
        elif coordsys == 'GAL':
71
            glon = self['glon'].real()
72
            glat = self['glat'].real()
73
            self._src_dir.lb_deg(glon, glat)
74

    
75
    def _get_parameters_bkgmethod_reflected(self):
76
        """
77
        Get parameters for REFLECTED background method
78
        """
79
        # Get source shape
80
        self._srcshape = self['srcshape'].string()
81

    
82
        # Set source region (so far only CIRCLE is supported)
83
        if self._srcshape == 'CIRCLE':
84

    
85
            # Query source direction
86
            self._query_src_direction()
87

    
88
            # Query minimum number of background regions
89
            self['bkgregmin'].integer()
90

    
91
            # Set circular source region
92
            self._rad = self['rad'].real()
93
            self._src_reg.append(gammalib.GSkyRegionCircle(self._src_dir, self._rad))
94

    
95
        # Return
96
        return
97

    
98
    def _get_parameters_bkgmethod_custom(self):
99
        """
100
        Get parameters for CUSTOM background method
101

102
        Raises
103
        ------
104
        RuntimeError
105
            Only one On region is allowed
106
        """
107
        # Set up source region
108
        filename      = self['srcregfile'].filename()
109
        self._src_reg = gammalib.GSkyRegions(filename)
110

    
111
        # Raise an exception if there is more than one source region
112
        if len(self._src_reg) != 1:
113
            raise RuntimeError('Only one On region is allowed')
114

    
115
        # Set up source direction. Query parameters if neccessary.
116
        if isinstance(self._src_reg[0], gammalib.GSkyRegionCircle):
117
            self._src_dir = self._src_reg[0].centre()
118
            self._rad     = self._src_reg[0].radius()
119
        else:
120
            self._query_src_direction()
121

    
122
        # Make sure that all CTA observations have an Off region by loading the
123
        # Off region region the parameter 'bkgregfile' for all CTA observations
124
        # without Off region
125
        for obs in self.obs():
126
            #if obs.instrument() == 'CTA':
127
            if obs.classname() == 'GCTAObservation':
128
                if obs.off_regions().is_empty():
129
                    filename = self['bkgregfile'].filename()
130
                    regions  = gammalib.GSkyRegions(filename)
131
                    obs.off_regions(regions)
132

    
133
        # Return
134
        return
135

    
136
    def _get_parameters_bkgmethod(self):
137
        """
138
        Get background method parameters
139
        """
140
        # Get background method
141
        bkgmethod = self['bkgmethod'].string()
142

    
143
        # Get background method dependent parameters
144
        if bkgmethod == 'REFLECTED':
145
            self._get_parameters_bkgmethod_reflected()
146
        elif bkgmethod == 'CUSTOM':
147
            self._get_parameters_bkgmethod_custom()
148

    
149
        # Query parameters that are needed for all background methods
150
        self['maxoffset'].real()
151

    
152
        # Return
153
        return
154

    
155
    def _get_parameters(self):
156
        """
157
        Get parameters from parfile and setup observations
158
        """
159
        # Setup observations (require response and allow event list, don't
160
        # allow counts cube)
161
        self._setup_observations(self.obs(), True, True, False)
162

    
163
        # Set energy bounds
164
        self._ebounds = self._create_ebounds()
165

    
166
        # Initialize empty src regions container
167
        self._src_reg = gammalib.GSkyRegions()
168

    
169
        # Exclusion map
170
        if self['inexclusion'].filename().is_fits():
171
            self._excl_reg = gammalib.GSkyRegionMap(self['inexclusion'].filename())
172
            self._has_exclusion = True
173
        else:
174
            self._has_exclusion = False
175

    
176
        # Query remaining parametersStacking
177
        self['stack'].boolean()
178

    
179
        # Query ahead output parameters
180
        if (self._read_ahead()):
181
            self['outobs'].string()
182
            self['prefix'].string()
183

    
184
        # If there are no observations in container then get them from the
185
        # parameter file
186
        if self.obs().is_empty():
187
            self.obs(self._get_observations(False))
188

    
189
        # Get background method parameters (have to come after setting up of
190
        # observations)
191
        self._get_parameters_bkgmethod()
192

    
193
        # Write input parameters into logger
194
        self._log_parameters(gammalib.TERSE)
195

    
196
        # Return
197
        return
198

    
199
    def _reflected_regions(self, obs):
200
        """
201
        Calculate list of reflected regions for a single observation (pointing)
202

203
        Parameters
204
        ----------
205
        obs : `~gammalib.GCTAObservation()`
206
            CTA observation
207

208
        Returns
209
        -------
210
        regions : `~gammalib.GSkyRegions`
211
            List of reflected regions
212
        """
213
        # Initialise list of reflected regions
214
        regions = gammalib.GSkyRegions()
215

    
216
        # Get offset angle of source
217
        pnt_dir = obs.pointing().dir()
218
        offset  = pnt_dir.dist_deg(self._src_dir)
219

    
220
        # If ...
221
        if offset <= self._rad or offset >= self['maxoffset'].real():
222
            self._log_string(gammalib.EXPLICIT, 'Observation %s pointed at %.3f '
223
                             'deg from source' % ((obs.id(), offset)))
224

    
225
        # ... otherwise
226
        else:
227
            posang = pnt_dir.posang_deg(self._src_dir)
228
            if self._srcshape == 'CIRCLE':
229
                # angular separation of reflected regions wrt camera center
230
                # and number
231
                alpha = 1.05 * 2.0 * self._rad / offset
232
                # 1.05 ensures background regions do not overlap due to
233
                # numerical precision issues
234
                N = int(2.0 * math.pi / alpha)
235
                if N < self['bkgregmin'].integer() + 3:
236
                    self._log_string(gammalib.EXPLICIT, 'Observation %s: '
237
                                     'insufficient regions for background '
238
                                     'estimation' % (obs.id()))
239

    
240
                # Otherwise loop over position angle to create reflected
241
                # regions
242
                else:
243
                    alpha = 360.0 / N
244
                    for s in range(2, N - 1):
245
                        dphi    = s * alpha
246
                        ctr_dir = pnt_dir.clone()
247
                        ctr_dir.rotate_deg(posang + dphi, offset)
248
                        region = gammalib.GSkyRegionCircle(ctr_dir, self._rad)
249
                        if self._has_exclusion:
250
                            if self._excl_reg.overlaps(region):
251
                                self._log_string(gammalib.VERBOSE, 'Observation '
252
                                                 '%s: reflected region overlaps '
253
                                                 'with exclusion region' %
254
                                                 (obs.id()))
255
                            else:
256
                                regions.append(region)
257
                        else:
258
                            regions.append(region)
259

    
260
        # Return reflected regions
261
        return regions
262

    
263
    def _set_models(self, obs):
264
        """
265
        Set models in observation container
266

267
        The method replaces all "CTA", "HESS", "VERITAS", and "MAGIC"
268
        background models by "CTAOnOff" background models.
269

270
        Parameters
271
        ----------
272
        obs : `~gammalib.GObservations()`
273
            Observation container
274

275
        Returns
276
        -------
277
        obs : `~gammalib.GObservations()`
278
            Observation container
279
        """
280
        # Initialise model container
281
        models = gammalib.GModels()
282

    
283
        # Loop over all models and replace CTA/HESS/VERITAS/MAGIC background
284
        # models by CTAOnOff background model
285
        for model in self.obs().models():
286
            if 'GCTA' in model.classname():
287
                if 'CTA'     in model.instruments() or \
288
                   'HESS'    in model.instruments() or \
289
                   'VERITAS' in model.instruments() or \
290
                   'MAGIC'   in model.instruments():
291
                    model.instruments('CTAOnOff')
292
            models.append(model)
293

    
294
        # Append model to observation container
295
        obs.models(models)
296

    
297
        # Return observation container
298
        return obs
299

    
300
    def _etrue_ebounds(self):
301
        """
302
        Set true energy boundaries
303

304
        Returns
305
        -------
306
        ebounds : `~gammalib.GEbounds()`
307
            True energy boundaries
308
        """
309
        # Determine minimum and maximum energies
310
        emin = self._ebounds.emin() * 0.5
311
        emax = self._ebounds.emax() * 1.2
312
        if emin.TeV() < self['etruemin'].real():
313
            emin = gammalib.GEnergy(self['etruemin'].real(), 'TeV')
314
        if emax.TeV() > self['etruemax'].real():
315
            emax = gammalib.GEnergy(self['etruemax'].real(), 'TeV')
316

    
317
        # Determine number of energy bins
318
        n_decades = (emax.log10TeV() - emin.log10TeV())
319
        n_bins    = int(n_decades * float(self['etruebins'].integer())) + 1
320

    
321
        # Set energy boundaries
322
        ebounds = gammalib.GEbounds(n_bins, emin, emax)
323

    
324
        # Write header
325
        self._log_header1(gammalib.TERSE, 'True energy binning')
326

    
327
        # Log true energy bins
328
        for i in range(ebounds.size()):
329
            value = '%s - %s' % (str(ebounds.emin(i)), str(ebounds.emax(i)))
330
            self._log_value(gammalib.TERSE, 'Bin %d' % (i+1), value)
331

    
332
        # Return energy boundaries
333
        return ebounds
334

    
335
    def _set_background_regions(self, obs):
336
        """
337
        Set background regions for an observation
338

339
        Parameters
340
        ----------
341
        obs : `~gammalib.GCTAObservation()`
342
            CTA observation
343

344
        Returns
345
        -------
346
        regions : `~gammalib.GSkyRegions()`
347
            Background regions
348
        """
349
        # Initialise empty background regions for this observation
350
        bkg_reg = gammalib.GSkyRegions()
351

    
352
        # If reflected background is requested then created reflected
353
        # background regions
354
        if self['bkgmethod'].string() == 'REFLECTED':
355
            bkg_reg = self._reflected_regions(obs)
356

    
357
        # ... otherwise if custom background is requested then get the
358
        # background regions from the observation. We use a copy here since
359
        # otherwise the background regions go out of scope once the observations
360
        # are replaced by the On/Off observations.
361
        elif self['bkgmethod'].string() == 'CUSTOM':
362
            bkg_reg = obs.off_regions().copy()
363

    
364
        # Return background regions
365
        return bkg_reg
366

    
367

    
368
    # Public methods
369
    def run(self):
370
        """
371
        Run the script
372
        """
373
        # Switch screen logging on in debug mode
374
        if self._logDebug():
375
            self._log.cout(True)
376

    
377
        # Get parameters
378
        self._get_parameters()
379

    
380
        # Write observation into logger
381
        self._log_observations(gammalib.NORMAL, self.obs(), 'Observation')
382

    
383
        # Set true energy bins
384
        etrue_ebounds = self._etrue_ebounds()
385

    
386
        # Write header
387
        self._log_header1(gammalib.TERSE, 'Spectral binning')
388

    
389
        # Log reconstructed energy bins
390
        for i in range(self._ebounds.size()):
391
            value = '%s - %s' % (str(self._ebounds.emin(i)),
392
                                 str(self._ebounds.emax(i)))
393
            self._log_value(gammalib.TERSE, 'Bin %d' % (i+1), value)
394

    
395
        # Write header
396
        self._log_header1(gammalib.NORMAL,
397
                          'Generation of source and background spectra')
398

    
399
        # Initialise run variables
400
        outobs         = gammalib.GObservations()
401
        self._bkg_regs = []
402

    
403
        # Loop through observations and generate pha, arf, rmf files
404
        for obs in self.obs():
405

    
406
            # Skip non CTA observations
407
            #if obs.instrument() != 'CTA':
408
            if obs.classname() != 'GCTAObservation':
409
                self._log_string(gammalib.NORMAL, 'Skip %s observation "%s"' % \
410
                                 (obs.instrument(), obs.id()))
411
                continue
412

    
413
            # Log current observation
414
            self._log_string(gammalib.NORMAL, 'Process observation "%s"' % \
415
                             (obs.id()))
416

    
417
            # Set background regions for this observation
418
            bkg_reg = self._set_background_regions(obs)
419

    
420
            # If there are background regions then create On/Off observation
421
            # and append it to the output container
422
            if bkg_reg.size() >= self['bkgregmin'].integer():
423
                onoff = gammalib.GCTAOnOffObservation(obs, self._src_dir,
424
                                                      etrue_ebounds,
425
                                                      self._ebounds,
426
                                                      self._src_reg,
427
                                                      bkg_reg)
428
                onoff.id(obs.id())
429
                outobs.append(onoff)
430
                self._bkg_regs.append({'regions': bkg_reg, 'id': obs.id()})
431
            else:
432
                self._log_string(gammalib.NORMAL, 'Observation %s not included '
433
                                 'in spectra generation' % (obs.id()))
434

    
435
        # Stack observations
436
        if outobs.size() > 1 and self['stack'].boolean() == True:
437

    
438
            # Write header
439
            self._log_header1(gammalib.NORMAL, 'Stacking %d observations' %
440
                              (outobs.size()))
441

    
442
            # Stack observations
443
            stacked_obs = gammalib.GCTAOnOffObservation(outobs)
444

    
445
            # Put stacked observations in output container
446
            outobs = gammalib.GObservations()
447
            outobs.append(stacked_obs)
448

    
449
        # Set models in output container
450
        outobs = self._set_models(outobs)
451

    
452
        # Set observation container
453
        self.obs(outobs)
454

    
455
        # Return
456
        return
457

    
458
    def save(self):
459
        """ 
460
        Save data
461
        """
462
        # Write header
463
        self._log_header1(gammalib.TERSE, 'Save data')
464

    
465
        # Get XML output filename, prefix and clobber
466
        outobs  = self['outobs'].string()
467
        prefix  = self['prefix'].string()
468
        clobber = self['clobber'].boolean()
469

    
470
        # Loop over all observation in container
471
        for obs in self.obs():
472

    
473
            # Set filenames
474
            if self['stack'].boolean():
475
                onname  = prefix + '_stacked_pha_on.fits'
476
                offname = prefix + '_stacked_pha_off.fits'
477
                arfname = prefix + '_stacked_arf.fits'
478
                rmfname = prefix + '_stacked_rmf.fits'
479
            elif self.obs().size() > 1:
480
                onname  = prefix + '_%s_pha_on.fits' % (obs.id())
481
                offname = prefix + '_%s_pha_off.fits' % (obs.id())
482
                arfname = prefix + '_%s_arf.fits' % (obs.id())
483
                rmfname = prefix + '_%s_rmf.fits' % (obs.id())
484
            else:
485
                onname  = prefix + '_pha_on.fits'
486
                offname = prefix + '_pha_off.fits'
487
                arfname = prefix + '_arf.fits'
488
                rmfname = prefix + '_rmf.fits'
489

    
490
            # Save files
491
            obs.on_spec().save(onname, clobber)
492
            obs.off_spec().save(offname, clobber)
493
            obs.arf().save(arfname, clobber)
494
            obs.rmf().save(rmfname, clobber)
495

    
496
            # Log file names
497
            self._log_value(gammalib.NORMAL, 'PHA on file', onname)
498
            self._log_value(gammalib.NORMAL, 'PHA off file', offname)
499
            self._log_value(gammalib.NORMAL, 'ARF file', arfname)
500
            self._log_value(gammalib.NORMAL, 'RMF file', arfname)
501

    
502
        # Save observation definition XML file
503
        self.obs().save(outobs)
504

    
505
        # Log file name
506
        self._log_value(gammalib.NORMAL, 'Obs. definition XML file', outobs)
507

    
508
        # Save ds9 On region file
509
        regname = prefix + '_on.reg'
510
        self._src_reg.save(regname)
511
        self._log_value(gammalib.NORMAL, 'On region file', regname)
512

    
513
        # Save ds9 Off region files
514
        for bkg_reg in self._bkg_regs:
515

    
516
            # Set filename
517
            if len(self._bkg_regs) > 1:
518
                regname = prefix + '_%s_off.reg' % (bkg_reg['id'])
519
            else:
520
                regname = prefix + '_off.reg'
521

    
522
            # Save ds9 region file
523
            bkg_reg['regions'].save(regname)
524

    
525
            # Log file name
526
            self._log_value(gammalib.NORMAL, 'Off region file', regname)
527

    
528
        # Return
529
        return
530

    
531

    
532
# ======================== #
533
# Main routine entry point #
534
# ======================== #
535
if __name__ == '__main__':
536

    
537
    # Create instance of application
538
    app = csphagen(sys.argv)
539

    
540
    # Execute application
541
    app.execute()