24 from cscripts
import obsutils
32 Generates a residual spectrum
40 self._init_csobservation(self.__class__.__name__, ctools.__version__,
55 Get parameters from parfile and setup the observation
59 self._setup_observations(self.obs(),
True,
True,
True)
62 self._set_obs_statistic(gammalib.toupper(self[
'statistic'].string()))
69 for obs
in self.obs():
70 if obs.classname() ==
'GCTAObservation':
71 if obs.eventtype() ==
'CountsCube':
75 elif obs.classname() ==
'GCTAOnOffObservation':
77 n_cta = n_unbinned + n_binned + n_onoff
78 n_other = self.obs().size() - n_cta
81 components = self[
'components'].boolean()
86 if self.obs().size() == 1
and n_binned == 1
and not components:
87 self.
_use_maps = self[
'modcube'].is_valid()
91 self[
'ebinalg'].string()
92 if self[
'ebinalg'].string() ==
'FILE':
93 self[
'ebinfile'].filename()
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
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()
109 if self.
_stack and n_unbinned == self.obs().size():
110 self[
'coordsys'].string()
111 self[
'proj'].string()
114 self[
'nxpix'].integer()
115 self[
'nypix'].integer()
120 if not self.
_use_maps and self.obs().models().size() == 0:
121 self.obs().models(self[
'inmodel'].filename())
127 self.
_mask = self[
'mask'].boolean()
132 self[
'regfile'].query()
139 self[
'edisp'].boolean()
142 self[
'algorithm'].string()
145 if self._read_ahead():
146 self[
'outfile'].filename()
149 self._log_parameters(gammalib.TERSE)
152 self._log_header1(gammalib.TERSE,
'Observation census')
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)
160 msg =
'WARNING: Only CTA observation can be handled, all non-CTA ' \
161 +
'observations will be ignored.'
162 self._log_string(gammalib.TERSE, msg)
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)
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)
185 Turn single event list into counts cube
189 obs : `~gammalib.GObservations`
190 Observation container with single event list
194 obs, info : `~gammalib.GObservations`, dict
195 Binned observation container and dictionary with event list ROI
196 and energy range information
200 ra = roi.centre().dir().ra_deg()
201 dec = roi.centre().dir().dec_deg()
205 npix = int(2.0 * rad / 0.02) + 1
208 self._log_string(gammalib.EXPLICIT,
'Binning events')
211 cntcube = ctools.ctbin(obs)
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()
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()
231 binned_obs = cntcube.obs().copy()
235 if self[
'emin'].real() > obs[0].events().emin().TeV():
238 emin = obs[0].events().emin().TeV()
239 if self[
'emax'].real() < obs[0].events().emax().TeV():
242 emax = obs[0].events().emax().TeV()
245 self._log_value(gammalib.EXPLICIT,
'Minimum energy (TeV)', emin)
246 self._log_value(gammalib.EXPLICIT,
'Maximum energy (TeV)', emax)
249 info = {
'was_list':
True,
'roi_ra': ra,
'roi_dec': dec,
'roi_rad': rad,
250 'emin': emin,
'emax': emax}
253 return binned_obs, info
255 def _masked_cube(self, cube, ra, dec, rad, emin='INDEF', emax='INDEF',
258 Mask an event cube and returns the masked cube
262 cube : `~gammalib.GCTAEventCube`
264 ra : float (str 'INDEF' for no selection on direction)
265 Right Ascension (deg)
266 dec : float (str 'INDEF' for no selection on direction)
268 rad : float (str 'INDEF' for no selection on direction)
270 emin : float (str 'INDEF' for no selection on energy)
272 emax : float (str 'INDEF' for no selection on energy)
277 cube : `~gammalib.GCTAEventCube`
281 obs = gammalib.GCTAObservation()
283 obs_cont = gammalib.GObservations()
287 cubemask = ctools.ctcubemask(obs_cont)
289 cubemask[
'dec'] = dec
290 cubemask[
'rad'] = rad
291 cubemask[
'emin'] = emin
292 cubemask[
'emax'] = emax
293 cubemask[
'regfile'] = regfile
298 cube = cubemask.obs()[0].events().copy()
305 Derive from event cube a count spectrum
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.
312 cube : `~gammalib.GCTAEventCube`
315 Dictionary with information on original event list
319 array : `~gammalib.GNdarray'
324 if evlist_info[
'was_list']:
325 msg =
'Masking ROI from original event list'
326 self._log_string(gammalib.EXPLICIT, msg)
328 evlist_info[
'roi_dec'],
329 evlist_info[
'roi_rad'],
330 emin=evlist_info[
'emin'],
331 emax=evlist_info[
'emax'])
335 if self[
'regfile'].is_valid():
336 regfile = self[
'regfile']
339 msg =
'Masking ROI requested by user'
340 self._log_string(gammalib.EXPLICIT, msg)
346 counts = cube.counts().copy()
347 counts = counts.clip(0.)
350 counts = counts.counts()
357 Create a Fits Table and store counts, model, and residuals
363 ebounds : `~gammalib.GEbounds'
365 counts : `~gammalib.GNdarray'
367 model : `~gammalib.GNdarray'
369 residuals : `~gammalib.GNdarray'
374 table : `~gammalib.GFitsBinTable'
375 Residual spectrum as FITS binary table
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')
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]
396 table = gammalib.GFitsBinTable(nrows)
397 table.extname(
'RESIDUALS' + obs_id)
400 table.card(
'ALGORITHM', self[
'algorithm'].string(),
401 'Algorithm used for computation of residuals')
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)
415 Append optional column to residual table
419 table : `~gammalib.GFitsBinTable'
424 Data to be filled into new column
428 table : `~gammalib.GFitsBinTable'
432 if table.nrows() != data.size():
433 msg =
'csresspec._append_column: FITS table and data have ' \
435 raise RuntimeError(msg)
438 column = gammalib.GFitsTableDoubleCol(name, table.nrows())
441 for i, value
in enumerate(data):
452 Turn results into FITS table
461 table : `~gammalib.GFitsBinTable`
465 msg =
'Filling residual table'
466 self._log_string(gammalib.NORMAL, msg)
473 result[
'residuals_on'])
476 if 'counts_off' in result:
477 table = self.
_append_column(table,
'Counts_Off', result[
'counts_off'])
480 if 'background' in result:
481 table = self.
_append_column(table,
'Model_Off', result[
'background'])
484 if 'residuals_off' in result:
485 table = self.
_append_column(table,
'Residuals_Off', result[
'residuals_off'])
488 for component
in result:
489 if 'component_' in component:
490 colname = component[10:]
502 results : list of dict
503 Residual spectra results
507 results : list of dict
511 for i, result
in enumerate(results):
515 stacked_result = result.copy()
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]
534 results = [stacked_result]
541 Calculate residuals for 3D observation
545 obs : `~gammalib.GCTAObservation`
547 models : `~gammalib.GModels`
551 ccube : `~gammalib.GCTAEventCube', optional
552 Count cube with stacked events lists
557 Residual result dictionary
560 obs_container = gammalib.GObservations()
561 obs_container.append(obs)
562 obs_container.models(models)
566 if obs.eventtype() ==
'CountsCube' or ccube
is not None:
567 evlist_info = {
'was_list':
False}
573 msg =
'Setting up binned observation'
574 self._log_string(gammalib.NORMAL, msg)
575 obs_container, evlist_info = self.
_bin_evlist(obs_container)
580 modcube = gammalib.GCTAEventCube(self[
'modcube'].filename())
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()
591 modcube = modelcube.cube().copy()
594 if ccube
is not None:
597 cntcube = obs_container[0].events().copy()
600 msg =
'Computing counts, model, and residual spectra'
601 self._log_string(gammalib.NORMAL, msg)
606 residuals = obsutils.residuals(self, counts, model)
609 ebounds = cntcube.ebounds().copy()
612 result = {
'obs_id': obs_id,
616 'residuals_on': residuals}
619 if self[
'components'].boolean():
622 for component
in models:
625 self._log_value(gammalib.NORMAL,
626 'Computing model component', component.name())
629 model_cont = gammalib.GModels()
630 model_cont.append(component)
631 modelcube.obs().models(model_cont)
634 if ccube
is not None:
635 modelcube.cube(ccube)
638 modelcube[
'edisp'] = self[
'edisp'].boolean()
642 modcube = modelcube.cube().copy()
646 result[
'component_%s' % component.name()] = model
653 Calculate residual for OnOff observation
657 obs : `~gammalib.GOnOffObservation`
659 models : `~gammalib.GModels`
667 Residual result dictionary
670 msg =
'Computing counts, model, and residual spectra'
671 self._log_string(gammalib.NORMAL, msg)
674 counts = obs.on_spec().counts_spectrum()
677 background = obs.model_background(models).counts_spectrum()
678 alpha = obs.on_spec().backscal_spectrum()
679 model = background.copy()
681 model += obs.model_gamma(models).counts_spectrum()
684 residuals = obsutils.residuals(self, counts, model)
687 ebounds = obs.on_spec().ebounds()
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()
695 residuals_off = obsutils.residuals(self, counts_off, background)
698 result = {
'obs_id': obs_id,
702 'residuals_on': residuals,
703 'counts_off': counts_off,
704 'background': background,
705 'residuals_off': residuals_off}
708 if self[
'components'].boolean():
711 for component
in models:
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':
725 self._log_value(gammalib.NORMAL,
726 'Computing model for component',
730 model_cont = gammalib.GModels()
731 model_cont.append(component)
734 model = obs.model_gamma(model_cont)
735 model = model.counts_spectrum()
738 result[
'component_%s' % component.name()] = model
741 bkg = background.copy()
742 backscal = obs.on_spec().backscal_spectrum()
744 result[
'component_Background'] = bkg
758 self._log_observations(gammalib.NORMAL, self.obs(),
'Observation')
761 self._log_header1(gammalib.TERSE,
'Processing Observations')
767 for i, obs
in enumerate(self.obs()):
774 if obs_id ==
'' and self.obs().size() > 1:
778 if self.obs().size() > 1:
779 self._log_header2(gammalib.NORMAL,
780 'Processing observation %s' % obs_id)
783 if obs.classname() ==
'GCTAObservation':
784 result = self.
_residuals_3D(obs, self.obs().models(), obs_id)
787 elif obs.classname() ==
'GCTAOnOffObservation':
791 results.append(result)
796 self._fits.append(table)
803 self._fits.append(table)
806 self._stamp(self.
_fits)
816 self._log_header1(gammalib.TERSE,
'Save residuals')
819 outfile = self[
'outfile'].filename()
822 self._log_value(gammalib.NORMAL,
'Residuals file', outfile.url())
825 self._fits.saveto(outfile, self[
'clobber'].boolean())
832 Return residual FITS file
836 fits : `~gammalib.GFits'
837 FITS file containing residuals
846 if __name__ ==
'__main__':