ctools 2.1.0.dev
Loading...
Searching...
No Matches
csresspec.py
Go to the documentation of this file.
1#! /usr/bin/env python
2# ==========================================================================
3# Generates a residual spectrum.
4#
5# Copyright (C) 2017-2022 Luigi Tibaldo
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
25
26
27# =============== #
28# csresspec class #
29# =============== #
30class csresspec(ctools.csobservation):
31 """
32 Generates a residual spectrum
33 """
34 # Constructor
35 def __init__(self, *argv):
36 """
37 Constructor
38 """
39 # Initialise application by calling the appropriate class constructor
40 self._init_csobservation(self.__class__.__name__, ctools.__version__,
41 argv)
42
43 # Initialise class members
44 self._use_maps = False
45 self._stack = False
46 self._mask = False
47 self._fits = gammalib.GFits()
48
49 # Return
50 return
51
52 # Private methods
53 def _get_parameters(self):
54 """
55 Get parameters from parfile and setup the observation
56 """
57 # Setup observations (require response and allow event list as well as
58 # counts cube)
59 self._setup_observations(self.obs(), True, True, True)
60
61 # Set observation statistic
62 self._set_obs_statistic(gammalib.toupper(self['statistic'].string()))
63
64 # Collect number of unbinned, binned and On/Off observations in
65 # observation container
66 n_unbinned = 0
67 n_binned = 0
68 n_onoff = 0
69 for obs in self.obs():
70 if obs.classname() == 'GCTAObservation':
71 if obs.eventtype() == 'CountsCube':
72 n_binned += 1
73 else:
74 n_unbinned += 1
75 elif obs.classname() == 'GCTAOnOffObservation':
76 n_onoff += 1
77 n_cta = n_unbinned + n_binned + n_onoff
78 n_other = self.obs().size() - n_cta
79
80 # Query whether to compute model for individual components
81 components = self['components'].boolean()
82
83 # If there is only one binned observation and no model for individual
84 # components is required, query for precomputed model file and set
85 # use_maps to True
86 if self.obs().size() == 1 and n_binned == 1 and not components:
87 self._use_maps = self['modcube'].is_valid()
88
89 # If there are unbinned observations query the energy binning parameters
90 if n_unbinned != 0:
91 self['ebinalg'].string()
92 if self['ebinalg'].string() == 'FILE':
93 self['ebinfile'].filename()
94 else:
95 self['emin'].real()
96 self['emax'].real()
97 self['enumbins'].integer()
98 if self['ebinalg'].string() == 'POW':
99 self['ebingamma'].real()
100 if n_cta > n_unbinned:
101 n_notunbin = n_cta - n_unbinned
102
103 # If there is more than one observation, and observations are all
104 # unbinned or all onoff query user to know if they wish stacked results
105 if self.obs().size() > 1 and \
106 (n_unbinned == self.obs().size() or n_onoff == self.obs().size()):
107 self._stack = self['stack'].boolean()
108 # If we are to stack event lists query parameters for cube creation
109 if self._stack and n_unbinned == self.obs().size():
110 self['coordsys'].string()
111 self['proj'].string()
112 self['xref'].real()
113 self['yref'].real()
114 self['nxpix'].integer()
115 self['nypix'].integer()
116 self['binsz'].real()
117
118 # If we are not using a precomputed model and no models are available
119 # in the observation container query input XML model file
120 if not self._use_maps and self.obs().models().size() == 0:
121 self.obs().models(self['inmodel'].filename())
122
123 # Unless all observations are On/Off query for mask definition
124 if n_onoff == n_cta:
125 pass
126 else:
127 self._mask = self['mask'].boolean()
128 if self._mask:
129 self['ra'].real()
130 self['dec'].real()
131 self['rad'].real()
132 self['regfile'].query()
133
134 # Unless all observations are On/Off, or we are using precomputed model
135 # maps query whether to use energy dispersion
136 if n_onoff == n_cta or self._use_maps:
137 pass
138 else:
139 self['edisp'].boolean()
140
141 # Query algorithm for residual computation
142 self['algorithm'].string()
143
144 # Read ahead output parameters
145 if self._read_ahead():
146 self['outfile'].filename()
147
148 # Write input parameters into logger
149 self._log_parameters(gammalib.TERSE)
150
151 # Write header for observation census
152 self._log_header1(gammalib.TERSE, 'Observation census')
153
154 # Log census of input observations
155 self._log_value(gammalib.NORMAL, 'Unbinned CTA observations', n_unbinned)
156 self._log_value(gammalib.NORMAL, 'Binned CTA observations', n_binned)
157 self._log_value(gammalib.NORMAL, 'On/off CTA observations', n_onoff)
158 self._log_value(gammalib.NORMAL, 'Other observations', n_other)
159 if n_other > 0:
160 msg = 'WARNING: Only CTA observation can be handled, all non-CTA ' \
161 + 'observations will be ignored.'
162 self._log_string(gammalib.TERSE, msg)
163
164 # Log for unbinned observations
165 if n_unbinned != 0:
166 msg = ' User defined energy binning will be used for %d unbinned ' \
167 'observations.' % (n_unbinned)
168 self._log_string(gammalib.TERSE, msg)
169 if n_cta > n_unbinned:
170 msg = ' The intrinsic binning will be used for the remaining ' \
171 '%d CTA observations.' % (n_notunbin)
172 self._log_string(gammalib.TERSE, msg)
173
174 # Signal how energy dispersion is applied
175 if n_onoff == n_cta or self._use_maps:
176 msg = ' Energy dispersion is applied based on the input data/model ' \
177 + 'and not according to the edisp parameter'
178 self._log_string(gammalib.TERSE, msg)
179
180 # Return
181 return
182
183 def _bin_evlist(self, obs):
184 """
185 Turn single event list into counts cube
186
187 Parameters
188 ----------
189 obs : `~gammalib.GObservations`
190 Observation container with single event list
191
192 Returns
193 -------
194 obs, info : `~gammalib.GObservations`, dict
195 Binned observation container and dictionary with event list ROI
196 and energy range information
197 """
198 # Retrieve information about ROI in event list
199 roi = obs[0].roi()
200 ra = roi.centre().dir().ra_deg()
201 dec = roi.centre().dir().dec_deg()
202 rad = roi.radius()
203
204 # We will cover the whole ROI with 0.02 deg binning
205 npix = int(2.0 * rad / 0.02) + 1
206
207 # Log binning of events
208 self._log_string(gammalib.EXPLICIT, 'Binning events')
209
210 # Bin events
211 cntcube = ctools.ctbin(obs)
212 cntcube['xref'] = ra
213 cntcube['yref'] = dec
214 cntcube['binsz'] = 0.02
215 cntcube['nxpix'] = npix
216 cntcube['nypix'] = npix
217 cntcube['proj'] = 'TAN'
218 cntcube['coordsys'] = 'CEL'
219 cntcube['ebinalg'] = self['ebinalg'].string()
220 if self['ebinalg'].string() == 'FILE':
221 cntcube['ebinfile'] = self['ebinfile'].filename()
222 else:
223 cntcube['enumbins'] = self['enumbins'].integer()
224 cntcube['emin'] = self['emin'].real()
225 cntcube['emax'] = self['emax'].real()
226 if self['ebinalg'].string() == 'POW':
227 cntcube['ebingamma'] = self['ebingamma'].real()
228 cntcube.run()
229
230 # Retrieve the binned observation container
231 binned_obs = cntcube.obs().copy()
232
233 # Check if energy boundaries provided by user extend beyond the
234 # content of the event list
235 if self['emin'].real() > obs[0].events().emin().TeV():
236 emin = 'INDEF'
237 else:
238 emin = obs[0].events().emin().TeV()
239 if self['emax'].real() < obs[0].events().emax().TeV():
240 emax = 'INDEF'
241 else:
242 emax = obs[0].events().emax().TeV()
243
244 # Log energy range
245 self._log_value(gammalib.EXPLICIT, 'Minimum energy (TeV)', emin)
246 self._log_value(gammalib.EXPLICIT, 'Maximum energy (TeV)', emax)
247
248 # Put ROI and E bound info in dictionary
249 info = {'was_list': True, 'roi_ra': ra, 'roi_dec': dec, 'roi_rad': rad,
250 'emin': emin, 'emax': emax}
251
252 # Return new oberservation container
253 return binned_obs, info
254
255 def _masked_cube(self, cube, ra, dec, rad, emin='INDEF', emax='INDEF',
256 regfile='NONE'):
257 """
258 Mask an event cube and returns the masked cube
259
260 Parameters
261 ----------
262 cube : `~gammalib.GCTAEventCube`
263 Event cube
264 ra : float (str 'INDEF' for no selection on direction)
265 Right Ascension (deg)
266 dec : float (str 'INDEF' for no selection on direction)
267 Declination (deg)
268 rad : float (str 'INDEF' for no selection on direction)
269 Radius (deg)
270 emin : float (str 'INDEF' for no selection on energy)
271 Minimum energy (TeV)
272 emax : float (str 'INDEF' for no selection on energy)
273 Maximum energy (TeV)
274
275 Returns
276 -------
277 cube : `~gammalib.GCTAEventCube`
278 Event cube
279 """
280 # Turn cube into observation container to feed to ctcubemask
281 obs = gammalib.GCTAObservation()
282 obs.events(cube)
283 obs_cont = gammalib.GObservations()
284 obs_cont.append(obs)
285
286 # Use ctcubemask to mask event cube pixels
287 cubemask = ctools.ctcubemask(obs_cont)
288 cubemask['ra'] = ra
289 cubemask['dec'] = dec
290 cubemask['rad'] = rad
291 cubemask['emin'] = emin
292 cubemask['emax'] = emax
293 cubemask['regfile'] = regfile
294 cubemask.run()
295
296 # Extract copy of cube from observation container (copy is needed to
297 # avoid memory leaks in SWIG)
298 cube = cubemask.obs()[0].events().copy()
299
300 # Return cube
301 return cube
302
303 def _cube_to_spectrum(self, cube, evlist_info):
304 """
305 Derive from event cube a count spectrum
306
307 If data come from event list use only the ROI and energy range of
308 the original data. Apply user defined mask if requested.
309
310 Parameters
311 ----------
312 cube : `~gammalib.GCTAEventCube`
313 Event cube
314 evlist_info : dict
315 Dictionary with information on original event list
316
317 Returns
318 -------
319 array : `~gammalib.GNdarray'
320 Counts spectrum
321 """
322 # If we started from event list mask the ROI only
323 # for residual computation
324 if evlist_info['was_list']:
325 msg = 'Masking ROI from original event list'
326 self._log_string(gammalib.EXPLICIT, msg)
327 cube = self._masked_cube(cube, evlist_info['roi_ra'],
328 evlist_info['roi_dec'],
329 evlist_info['roi_rad'],
330 emin=evlist_info['emin'],
331 emax=evlist_info['emax'])
332
333 # Apply user mask
334 if self._mask:
335 if self['regfile'].is_valid():
336 regfile = self['regfile']
337 else:
338 regfile = 'NONE'
339 msg = 'Masking ROI requested by user'
340 self._log_string(gammalib.EXPLICIT, msg)
341 cube = self._masked_cube(cube, self['ra'], self['dec'],
342 self['rad'],
343 regfile=regfile)
344
345 # Extract skymap and clip at 0 to null masked areas
346 counts = cube.counts().copy()
347 counts = counts.clip(0.)
348
349 # Convert skymap into GNdarray count spectrum
350 counts = counts.counts()
351
352 # Return
353 return counts
354
355 def _residuals_table(self, obs_id, ebounds, counts, model, residuals):
356 """
357 Create a Fits Table and store counts, model, and residuals
358
359 Parameters
360 ----------
361 obs_id : str
362 Observation id
363 ebounds : `~gammalib.GEbounds'
364 Energy boundaries
365 counts : `~gammalib.GNdarray'
366 Counts spectrum
367 model : `~gammalib.GNdarray'
368 Model spectrum
369 residuals : `~gammalib.GNdarray'
370 Residual spectrum
371
372 Returns
373 -------
374 table : `~gammalib.GFitsBinTable'
375 Residual spectrum as FITS binary table
376 """
377 # Create FITS table columns
378 nrows = ebounds.size()
379 energy_low = gammalib.GFitsTableDoubleCol('Emin', nrows)
380 energy_high = gammalib.GFitsTableDoubleCol('Emax', nrows)
381 counts_col = gammalib.GFitsTableDoubleCol('Counts', nrows)
382 model_col = gammalib.GFitsTableDoubleCol('Model', nrows)
383 resid_col = gammalib.GFitsTableDoubleCol('Residuals', nrows)
384 energy_low.unit('TeV')
385 energy_high.unit('TeV')
386
387 # Fill FITS table columns
388 for i in range(nrows):
389 energy_low[i] = ebounds.emin(i).TeV()
390 energy_high[i] = ebounds.emax(i).TeV()
391 counts_col[i] = counts[i]
392 model_col[i] = model[i]
393 resid_col[i] = residuals[i]
394
395 # Initialise FITS Table with extension set to obs id
396 table = gammalib.GFitsBinTable(nrows)
397 table.extname('RESIDUALS' + obs_id)
398
399 # Add Header card to specify algorithm used for residual computation
400 table.card('ALGORITHM', self['algorithm'].string(),
401 'Algorithm used for computation of residuals')
402
403 # Append filled columns to fits table
404 table.append(energy_low)
405 table.append(energy_high)
406 table.append(counts_col)
407 table.append(model_col)
408 table.append(resid_col)
409
410 # Return binary table
411 return table
412
413 def _append_column(self, table, name, data):
414 """
415 Append optional column to residual table
416
417 Parameters
418 ----------
419 table : `~gammalib.GFitsBinTable'
420 FITS binary table
421 name : str
422 Column name
423 data : float
424 Data to be filled into new column
425
426 Returns
427 -------
428 table : `~gammalib.GFitsBinTable'
429 FITS binary table
430 """
431 # If size is incompatible then throw an error
432 if table.nrows() != data.size():
433 msg = 'csresspec._append_column: FITS table and data have ' \
434 'incompatible size.'
435 raise RuntimeError(msg)
436
437 # Create column
438 column = gammalib.GFitsTableDoubleCol(name, table.nrows())
439
440 # Fill data
441 for i, value in enumerate(data):
442 column[i] = value
443
444 # Append new column to table
445 table.append(column)
446
447 # Return modified table
448 return table
449
450 def _results2table(self, result):
451 """
452 Turn results into FITS table
453
454 Parameters
455 ----------
456 result : dict
457 Result dictionary
458
459 Returns
460 -------
461 table : `~gammalib.GFitsBinTable`
462 FITS binary table
463 """
464 # Log action
465 msg = 'Filling residual table'
466 self._log_string(gammalib.NORMAL, msg)
467
468 # Fill results table
469 table = self._residuals_table(result['obs_id'],
470 result['ebounds'],
471 result['counts_on'],
472 result['model'],
473 result['residuals_on'])
474
475 # Optionally add Off spectrum to table
476 if 'counts_off' in result:
477 table = self._append_column(table, 'Counts_Off', result['counts_off'])
478
479 # Optionally add background/Off model to table
480 if 'background' in result:
481 table = self._append_column(table, 'Model_Off', result['background'])
482
483 # Optionally add Off residuals to table
484 if 'residuals_off' in result:
485 table = self._append_column(table, 'Residuals_Off', result['residuals_off'])
486
487 # Add components
488 for component in result:
489 if 'component_' in component:
490 colname = component[10:]
491 table = self._append_column(table, colname, result[component])
492
493 # Return FITS table
494 return table
495
496 def _stack_results(self, results):
497 """
498 Stack results
499
500 Parameters
501 ----------
502 results : list of dict
503 Residual spectra results
504
505 Returns
506 -------
507 results : list of dict
508 Stacked result
509 """
510 # Loop over results
511 for i, result in enumerate(results):
512
513 # Copy results for first iteration
514 if i == 0:
515 stacked_result = result.copy()
516
517 # ... otherwise add results
518 else:
519 stacked_result['obs_id'] = ''
520 stacked_result['counts_on'] += result['counts_on']
521 stacked_result['model'] += result['model']
522 stacked_result['residuals_on'] += result['residuals_on']
523 if 'counts_off' in result:
524 stacked_result['counts_off'] += result['counts_off']
525 if 'background' in result:
526 stacked_result['background'] += result['background']
527 if 'residuals_off' in result:
528 stacked_result['residuals_off'] += result['residuals_off']
529 for component in result:
530 if 'component_' in component:
531 stacked_result[component] += result[component]
532
533 # Create list of stacked results
534 results = [stacked_result]
535
536 # Return stacked results
537 return results
538
539 def _residuals_3D(self, obs, models, obs_id, ccube=None):
540 """
541 Calculate residuals for 3D observation
542
543 Parameters
544 ----------
545 obs : `~gammalib.GCTAObservation`
546 CTA observation
547 models : `~gammalib.GModels`
548 Models
549 obs_id : str
550 Observation ID
551 ccube : `~gammalib.GCTAEventCube', optional
552 Count cube with stacked events lists
553
554 Returns
555 -------
556 result : dict
557 Residual result dictionary
558 """
559 # Create observation container with observation
560 obs_container = gammalib.GObservations()
561 obs_container.append(obs)
562 obs_container.models(models)
563
564 # If binned data already exist set the evlist_info dictionary to have
565 # attribute was_list False
566 if obs.eventtype() == 'CountsCube' or ccube is not None:
567 evlist_info = {'was_list': False}
568
569 # ... otherwise bin now
570 else:
571 # we remember if we binned an event list so that we can
572 # mask only the ROI for residual calculation
573 msg = 'Setting up binned observation'
574 self._log_string(gammalib.NORMAL, msg)
575 obs_container, evlist_info = self._bin_evlist(obs_container)
576
577 # Calculate Model and residuals. If model cube is provided load
578 # it
579 if self._use_maps:
580 modcube = gammalib.GCTAEventCube(self['modcube'].filename())
581
582 # ... otherwise calculate it now
583 else:
584 msg = 'Computing model cube'
585 self._log_string(gammalib.NORMAL, msg)
586 modelcube = ctools.ctmodel(obs_container)
587 if ccube is not None:
588 modelcube.cube(ccube)
589 modelcube['edisp'] = self['edisp'].boolean()
590 modelcube.run()
591 modcube = modelcube.cube().copy()
592
593 # Extract cntcube for residual computation
594 if ccube is not None:
595 cntcube = ccube
596 else:
597 cntcube = obs_container[0].events().copy()
598
599 # Derive count spectra from cubes
600 msg = 'Computing counts, model, and residual spectra'
601 self._log_string(gammalib.NORMAL, msg)
602 counts = self._cube_to_spectrum(cntcube, evlist_info)
603 model = self._cube_to_spectrum(modcube, evlist_info)
604
605 # Calculate residuals
606 residuals = obsutils.residuals(self, counts, model)
607
608 # Extract energy bounds
609 ebounds = cntcube.ebounds().copy()
610
611 # Set result dictionary
612 result = {'obs_id': obs_id,
613 'ebounds': ebounds,
614 'counts_on': counts,
615 'model': model,
616 'residuals_on': residuals}
617
618 # Calculate models of individual components if requested
619 if self['components'].boolean():
620
621 # Loop over components
622 for component in models:
623
624 # Log action
625 self._log_value(gammalib.NORMAL,
626 'Computing model component', component.name())
627
628 # Set model cube models to individual component
629 model_cont = gammalib.GModels()
630 model_cont.append(component)
631 modelcube.obs().models(model_cont)
632
633 # Reset base cube that was modified internally by ctmodel
634 if ccube is not None:
635 modelcube.cube(ccube)
636
637 # Run model cube
638 modelcube['edisp'] = self['edisp'].boolean()
639 modelcube.run()
640
641 # Extract spectrum of individual component
642 modcube = modelcube.cube().copy()
643 model = self._cube_to_spectrum(modcube, evlist_info)
644
645 # Append to results
646 result['component_%s' % component.name()] = model
647
648 # Return result
649 return result
650
651 def _residuals_OnOff(self, obs, models, obs_id):
652 """
653 Calculate residual for OnOff observation
654
655 Parameters
656 ----------
657 obs : `~gammalib.GOnOffObservation`
658 OnOff observation
659 models : `~gammalib.GModels`
660 Models
661 obs_id : str
662 Observation ID
663
664 Returns
665 -------
666 result : dict
667 Residual result dictionary
668 """
669 # Log action
670 msg = 'Computing counts, model, and residual spectra'
671 self._log_string(gammalib.NORMAL, msg)
672
673 # On spectrum
674 counts = obs.on_spec().counts_spectrum()
675
676 # Model for On region
677 background = obs.model_background(models).counts_spectrum()
678 alpha = obs.on_spec().backscal_spectrum()
679 model = background.copy()
680 model *= alpha
681 model += obs.model_gamma(models).counts_spectrum()
682
683 # On Residuals
684 residuals = obsutils.residuals(self, counts, model)
685
686 # Extract energy bounds
687 ebounds = obs.on_spec().ebounds()
688
689 # Get Off spectrum and add to table
690 msg = 'Computing counts, model, and residual spectra for Off regions'
691 self._log_string(gammalib.NORMAL, msg)
692 counts_off = obs.off_spec().counts_spectrum()
693
694 # Calculate Off residuals and add to table
695 residuals_off = obsutils.residuals(self, counts_off, background)
696
697 # Set result dictionary
698 result = {'obs_id': obs_id,
699 'ebounds': ebounds,
700 'counts_on': counts,
701 'model': model,
702 'residuals_on': residuals,
703 'counts_off': counts_off,
704 'background': background,
705 'residuals_off': residuals_off}
706
707 # Calculate models of individual components if requested
708 if self['components'].boolean():
709
710 # Loop over model components
711 for component in models:
712
713 # If the component is a background model then pass. We always
714 # add the background at the end so that we accommodate WSTAT
715 # for which the background is not mandatory in the model
716 if component.classname() == 'GCTAModelAeffBackground' or \
717 component.classname() == 'GCTAModelBackground' or \
718 component.classname() == 'GCTAModelCubeBackground' or \
719 component.classname() == 'GCTAModelIrfBackground' or \
720 component.classname() == 'GCTAModelRadialAcceptance':
721 pass
722
723 # ... otherwise calculate gamma component and append to Table
724 else:
725 self._log_value(gammalib.NORMAL,
726 'Computing model for component',
727 component.name())
728
729 # Create observation container for individual components
730 model_cont = gammalib.GModels()
731 model_cont.append(component)
732
733 # Calculate gamma model
734 model = obs.model_gamma(model_cont)
735 model = model.counts_spectrum()
736
737 # Append to results
738 result['component_%s' % component.name()] = model
739
740 # Add now the background that is already calculated
741 bkg = background.copy()
742 backscal = obs.on_spec().backscal_spectrum()
743 bkg *= backscal
744 result['component_Background'] = bkg
745
746 # Return result
747 return result
748
749 # Public methods
750 def process(self):
751 """
752 Process the script
753 """
754 # Get parameters
755 self._get_parameters()
756
757 # Write observation into logger
758 self._log_observations(gammalib.NORMAL, self.obs(), 'Observation')
759
760 # Log processing header
761 self._log_header1(gammalib.TERSE, 'Processing Observations')
762
763 # Initialise list of results
764 results = []
765
766 # Loop over observations and calculate residuals
767 for i, obs in enumerate(self.obs()):
768
769 # Retrieve and store obs id
770 obs_id = obs.id()
771
772 # If observation id is empty and there is more than one observation
773 # replace with incremental number
774 if obs_id == '' and self.obs().size() > 1:
775 obs_id = '%6.6d' % i
776
777 # Log processing of observation
778 if self.obs().size() > 1:
779 self._log_header2(gammalib.NORMAL,
780 'Processing observation %s' % obs_id)
781
782 # If 3D observation
783 if obs.classname() == 'GCTAObservation':
784 result = self._residuals_3D(obs, self.obs().models(), obs_id)
785
786 # otherwise, if On/Off
787 elif obs.classname() == 'GCTAOnOffObservation':
788 result = self._residuals_OnOff(obs, self.obs().models(), obs_id)
789
790 # Append result to list of results
791 results.append(result)
792
793 # If no stacking is requested the append table to FITS file
794 if not self._stack:
795 table = self._results2table(results[i])
796 self._fits.append(table)
797
798 # If stacking is requested then stack results and append table to
799 # FITS file
800 if self._stack:
801 results = self._stack_results(results)
802 table = self._results2table(results[0])
803 self._fits.append(table)
804
805 # Stamp FITS file
806 self._stamp(self._fits)
807
808 # Return
809 return
810
811 def save(self):
812 """
813 Save residuals
814 """
815 # Write header
816 self._log_header1(gammalib.TERSE, 'Save residuals')
817
818 # Get outfile parameter
819 outfile = self['outfile'].filename()
820
821 # Log file name
822 self._log_value(gammalib.NORMAL, 'Residuals file', outfile.url())
823
824 # Save residuals
825 self._fits.saveto(outfile, self['clobber'].boolean())
826
827 # Return
828 return
829
830 def resspec(self):
831 """
832 Return residual FITS file
833
834 Returns
835 -------
836 fits : `~gammalib.GFits'
837 FITS file containing residuals
838 """
839 # Return
840 return self._fits
841
842
843# ======================== #
844# Main routine entry point #
845# ======================== #
846if __name__ == '__main__':
847
848 # Create instance of application
849 app = csresspec(sys.argv)
850
851 # Execute application
852 app.execute()
_append_column(self, table, name, data)
Definition csresspec.py:413
_residuals_OnOff(self, obs, models, obs_id)
Definition csresspec.py:651
_stack_results(self, results)
Definition csresspec.py:496
_masked_cube(self, cube, ra, dec, rad, emin='INDEF', emax='INDEF', regfile='NONE')
Definition csresspec.py:256
_residuals_3D(self, obs, models, obs_id, ccube=None)
Definition csresspec.py:539
_cube_to_spectrum(self, cube, evlist_info)
Definition csresspec.py:303
_residuals_table(self, obs_id, ebounds, counts, model, residuals)
Definition csresspec.py:355