Commit 31bf170bf81b7e261e061c3a56e9d9b14f609375

Authored by Jürgen Knödlseder
1 parent d5075297

Add inmap parameter to comlixmap (#4187)

ChangeLog
1   -2022-11-18
  1 +2023-01-05
2 2  
3 3 * Version 2.1.0 released
4 4 ========================
5 5  
  6 + Add inmap parameter to comlixmap (#4187)
6 7 Add comobsconv script (#4159)
7 8 Optionally write fitted null hypothesis model in cttsmap
8 9 Avoid NaN in ctbutterfly (#4069)
... ...
1 1 New Features and Important Changes in ctools 2.1.0
2 2  
3   -18 November 2022
  3 +5 January 2023
4 4  
5 5  
6 6 Introduction
... ... @@ -331,7 +331,9 @@ None
331 331  
332 332 comlixmap - Generation of COMPTEL Test Statistic map using SRCLIX algorithm
333 333 ---------------------------------------------------------------------------
334   -None
  334 +Added an "inmap" parameter to allow restart the computations from an existing
  335 +TS map without recomputation of values that exist. The script now also writes
  336 +parameter uncertainties in the resulting TS map FITS file (#4187).
335 337  
336 338  
337 339 comobsadd - Combine COMPTEL observations
... ...
README.md
1 1 ctools information
2 2 ==================
3   -* Version: 2.1.0.dev (18 November 2022)
  3 +* Version: 2.1.0.dev (5 January 2023)
4 4 * GammaLib dependency: 2.1.0.dev
5 5  
6 6 [![Build Status](https://cta-jenkins.irap.omp.eu/buildStatus/icon?job=ctools-integrate-os)](https://cta-jenkins.irap.omp.eu/job/ctools-integrate-os/)
... ...
doc/source/admin/release_history/2.1.rst
... ... @@ -16,6 +16,8 @@ In particular, this release provides:
16 16 Bug fixes
17 17 ---------
18 18  
  19 +* [`4187 <https://cta-redmine.irap.omp.eu/issues/4187>`_] -
  20 + Add ``inmap`` parameter to :ref:`comlixmap` to allow restart from an existing TS map
19 21 * [`4159 <https://cta-redmine.irap.omp.eu/issues/4159>`_] -
20 22 Add :ref:`comobsconv` script
21 23 * Optionally write fitted null hypothesis model in cttsmap
... ...
doc/source/users/reference_manual/comlixmap.rst
... ... @@ -21,6 +21,12 @@ General parameters
21 21 ``inmodel [file]``
22 22 Input model definition XML file.
23 23  
  24 +``(inmap = NONE) [file]``
  25 + Input Test Statistic map file. If a TS map FITS file is specified, information
  26 + will be extracted from the file for pixels that are contained in that map,
  27 + avoiding recomputation. This allows for example to enlarge an existing TS map
  28 + for which the computations will be done on the enlarged section.
  29 +
24 30 ``srcname [string]``
25 31 Test source name.
26 32  
... ...
modules/comscripts/comlixmap.py
... ... @@ -39,10 +39,11 @@ class comlixmap(ctools.cslikelihood):
39 39 self._init_cslikelihood(self.__class__.__name__, ctools.__version__, argv)
40 40  
41 41 # Initialise members
42   - self._inmap = None
43   - self._maps = []
44   - self._map_names = []
45   - self._srcnames = []
  42 + self._inmaps = []
  43 + self._inmap_names = []
  44 + self._maps = []
  45 + self._map_names = []
  46 + self._srcnames = []
46 47  
47 48 # Return
48 49 return
... ... @@ -67,9 +68,9 @@ class comlixmap(ctools.cslikelihood):
67 68 msg = 'Source "%s" not found in models.' % srcname
68 69 raise RuntimeError(msg)
69 70  
70   - # Get optional input TS map filename
  71 + # Query optional input TS map filename
71 72 if self['inmap'].is_valid():
72   - self._inmap = self['inmap'].filename()
  73 + self['inmap'].filename()
73 74  
74 75 # Query parameters
75 76 self['like_accuracy'].real()
... ... @@ -158,6 +159,62 @@ class comlixmap(ctools.cslikelihood):
158 159 # Return
159 160 return
160 161  
  162 + def _load_inmaps(self, filename):
  163 + """
  164 + Load input TS maps
  165 +
  166 + Loads input TS maps from a FITS files and checks that number
  167 + of extensions and extension names are consistent with the
  168 + expectations.
  169 +
  170 + Parameters
  171 + ----------
  172 + filename : `~gammalib/GFilename`
  173 + Input filename
  174 + """
  175 + # Log header
  176 + self._log_header1(gammalib.NORMAL, 'Load input TS map(s)')
  177 + self._log_value(gammalib.NORMAL, 'Input filename', filename.url())
  178 +
  179 + # Initialise list of input maps and map names
  180 + self._inmaps = []
  181 + self._inmap_names = []
  182 +
  183 + # Open FITS file
  184 + fits = gammalib.GFits(filename)
  185 +
  186 + # Raise an exception if the number of extensions does not
  187 + # match the expectations
  188 + if fits.size() != len(self._map_names):
  189 + msg = 'There are %d extensions in the input TS map but %d '\
  190 + 'extensions are expected. Please provide a compatible '\
  191 + 'input TS map.' % (fits.size(), len(self._map_names))
  192 + raise RuntimeError(msg)
  193 +
  194 + # Loop over extensions and extract all images
  195 + for i, hdu in enumerate(fits):
  196 +
  197 + # Get extension name
  198 + name = hdu.extname()
  199 +
  200 + # Raise exception if the extension name is conform to
  201 + # expectation
  202 + if name != self._map_names[i]:
  203 + msg = 'Encountered extension name "%s" of map %d '\
  204 + 'does not correspond to expected extension '\
  205 + 'name "%s". Please provide a compatible '\
  206 + 'input TS map.' % (name, i, self._map_names[i])
  207 + raise RuntimeError(msg)
  208 +
  209 + # Append extension name and map
  210 + map = gammalib.GSkyMap(hdu)
  211 + self._inmap_names.append(name)
  212 + self._inmaps.append(map)
  213 + self._log_value(gammalib.NORMAL, 'Loaded', name)
  214 +
  215 + # Return
  216 + return
  217 +
161 218 def _create_maps(self):
162 219 """
163 220 Create sky maps based on user parameters
... ... @@ -181,6 +238,10 @@ class comlixmap(ctools.cslikelihood):
181 238 # Initialise number of free parameters
182 239 nfree = 0
183 240  
  241 + # Append TS map names
  242 + for srcname in self._srcnames:
  243 + self._map_names.append(srcname)
  244 +
184 245 # Compute number of free model parameters for all test sources,
185 246 # excluding 'RA', 'DEC', 'GLON' and 'GLAT' and add map names
186 247 # composed of 'srcname_parname_val' and 'srcname_parname_unc'
... ... @@ -325,6 +386,68 @@ class comlixmap(ctools.cslikelihood):
325 386 # Return TS
326 387 return ts
327 388  
  389 + def _extract_from_inmaps(self, dir, ipix):
  390 + """
  391 + Extract information from input maps
  392 +
  393 + Parameters
  394 + ----------
  395 + dir : `~gammalib.GSkyDir`
  396 + Position of test source
  397 + ipix : int
  398 + TS map pixel
  399 +
  400 + Returns
  401 + -------
  402 + extracted : boolean
  403 + True if extraction was successful
  404 + """
  405 + # Initialise extraction flag
  406 + extracted = False
  407 +
  408 + # If there are input TS maps then check if the direction is
  409 + # contained in the maps
  410 + if len(self._inmaps) == len(self._maps):
  411 +
  412 + # Get first input TS map
  413 + map = self._inmaps[0]
  414 +
  415 + # If position of test source is contained in input TS map
  416 + # then check whether the corresponding pixel distance is
  417 + # sufficiently small
  418 + if map.contains(dir):
  419 +
  420 + # If distance between the position of the test source
  421 + # to input TS map pixel is smaller than 0.1 degrees
  422 + # then recover information
  423 + inx = map.dir2inx(dir)
  424 + map_dir = map.inx2dir(inx)
  425 + dist = map_dir.dist_deg(dir)
  426 + if dist < 0.1:
  427 +
  428 + # Recover information
  429 + for i in range(len(self._inmaps)):
  430 + self._maps[i][ipix] = self._inmaps[i][inx]
  431 +
  432 + # Log that information was extraced
  433 + key = 'Input map pixel'
  434 + value = '%d (offset: %f deg)' % (inx, dist)
  435 + self._log_value(gammalib.NORMAL, key, value)
  436 + for i in range(len(self._maps)):
  437 + if i < len(self._srcnames):
  438 + key = 'TS %s' % (self._map_names[i])
  439 + value = '%.3f' % (self._maps[i][ipix])
  440 + else:
  441 + key = '%s' % (self._map_names[i])
  442 + value = '%e' % (self._maps[i][ipix])
  443 + self._log_value(gammalib.NORMAL, key, value)
  444 +
  445 + # Signal that information was extracted
  446 + extracted = True
  447 +
  448 + # Return
  449 + return extracted
  450 +
328 451 def _final_model_fit(self):
329 452 """
330 453 Perform final model fit using ctlike
... ... @@ -391,6 +514,11 @@ class comlixmap(ctools.cslikelihood):
391 514 # Log sky map
392 515 self._log_string(gammalib.NORMAL, str(self._maps[0]))
393 516  
  517 + # Optionally load input TS map(s)
  518 + if self['inmap'].is_valid():
  519 + self._load_inmaps(self['inmap'].filename())
  520 + self._log_string(gammalib.NORMAL, str(self._inmaps[0]))
  521 +
394 522 # Log header
395 523 self._log_header1(gammalib.NORMAL, 'Generate TS map')
396 524  
... ... @@ -417,24 +545,28 @@ class comlixmap(ctools.cslikelihood):
417 545 (ipix, self._maps[0].npix(), str(dir))
418 546 self._log_header3(gammalib.NORMAL, header)
419 547  
420   - # Compute TS
421   - ts = self._compute_ts(dir)
422   -
423   - # Store TS values in sky map
424   - for i, value in enumerate(ts):
425   - self._maps[i][ipix] = value
426   -
427   - # Store fitted model parameters and uncertainties in sky maps
428   - ipar = len(ts)
429   - for srcname in self._srcnames:
430   - source = self.obs().models()[srcname]
431   - for par in source:
432   - if par.name() != 'RA' and par.name() != 'DEC' and \
433   - par.name() != 'GLON' and par.name() != 'GLAT' and \
434   - par.is_free():
435   - self._maps[ipar][ipix] = par.value()
436   - self._maps[ipar+1][ipix] = par.error()
437   - ipar += 2
  548 + # Compute TS if information could not be recovered from input
  549 + # maps
  550 + if not self._extract_from_inmaps(dir, ipix):
  551 +
  552 + # Compute TS
  553 + ts = self._compute_ts(dir)
  554 +
  555 + # Store TS values in sky map
  556 + for i, value in enumerate(ts):
  557 + self._maps[i][ipix] = value
  558 +
  559 + # Store fitted model parameters and uncertainties in sky maps
  560 + ipar = len(ts)
  561 + for srcname in self._srcnames:
  562 + source = self.obs().models()[srcname]
  563 + for par in source:
  564 + if par.name() != 'RA' and par.name() != 'DEC' and \
  565 + par.name() != 'GLON' and par.name() != 'GLAT' and \
  566 + par.is_free():
  567 + self._maps[ipar][ipix] = par.value()
  568 + self._maps[ipar+1][ipix] = par.error()
  569 + ipar += 2
438 570  
439 571 # Return
440 572 return
... ... @@ -469,10 +601,8 @@ class comlixmap(ctools.cslikelihood):
469 601 map.write(fits)
470 602  
471 603 # Set extension name for all maps
472   - for i, srcname in enumerate(self._srcnames):
473   - fits[i].extname(srcname)
474 604 for i, name in enumerate(self._map_names):
475   - fits[i+len(self._srcnames)].extname(name)
  605 + fits[i].extname(name)
476 606  
477 607 # Save FITS file
478 608 fits.saveto(outmap, self['clobber'].boolean())
... ...
test/test_comlixmap.py
... ... @@ -2,7 +2,7 @@
2 2 # ==========================================================================
3 3 # This scripts performs unit tests for the comlixmap script.
4 4 #
5   -# Copyright (C) 2021 Juergen Knoedlseder
  5 +# Copyright (C) 2021-2022 Juergen Knoedlseder
6 6 #
7 7 # This program is free software: you can redistribute it and/or modify
8 8 # it under the terms of the GNU General Public License as published by
... ... @@ -191,6 +191,31 @@ class Test(test):
191 191 # Check result
192 192 self._check_result('comlixmap_py3.fits', 1, 2)
193 193  
  194 + # Test loading of input map
  195 + lix = comscripts.comlixmap()
  196 + lix['inobs'] = self._obs
  197 + lix['inmodel'] = self._model
  198 + lix['inmap'] = 'comlixmap_py3.fits'
  199 + lix['srcname'] = 'Crab'
  200 + lix['bkgmethod'] = 'BGDLIXE'
  201 + lix['coordsys'] = 'CEL'
  202 + lix['proj'] = 'TAN'
  203 + lix['xref'] = 83.6331
  204 + lix['yref'] = 22.0145
  205 + lix['nxpix'] = 1
  206 + lix['nypix'] = 2
  207 + lix['binsz'] = 1.0
  208 + lix['outmap'] = 'comlixmap_py4.fits'
  209 + lix['logfile'] = 'comlixmap_py4.log'
  210 + lix['chatter'] = 4
  211 +
  212 + # Run script and save result
  213 + lix.logFileOpen() # Make sure we get a log file
  214 + lix.execute()
  215 +
  216 + # Check result
  217 + self._check_result('comlixmap_py4.fits', 1, 2)
  218 +
194 219 # Return
195 220 return
196 221  
... ... @@ -199,10 +224,19 @@ class Test(test):
199 224 """
200 225 Check result file
201 226 """
202   - # Load residual map file
  227 + # Load TS map as FITS file and check extension names
  228 + fits = gammalib.GFits(filename)
  229 + self.test_value(fits.size(), 5, 'Check number of maps in FITS file')
  230 + self.test_value(fits[0].extname(), 'Crab', 'Check TS map extension name')
  231 + self.test_value(fits[1].extname(), 'Crab_Prefactor_val', 'Check Prefactor value extension name')
  232 + self.test_value(fits[2].extname(), 'Crab_Prefactor_unc', 'Check Prefactor error extension name')
  233 + self.test_value(fits[3].extname(), 'Crab_Index_val', 'Check Index value extension name')
  234 + self.test_value(fits[4].extname(), 'Crab_Index_unc', 'Check Index error extension name')
  235 +
  236 + # Load TS map as sky map
203 237 map = gammalib.GSkyMap(filename)
204 238  
205   - # Check that there is one observation
  239 + # Check the map dimensions
206 240 self.test_value(map.nx(), nx, 'Check for number of X pixels in sky map')
207 241 self.test_value(map.ny(), ny, 'Check for number of Y pixels in sky map')
208 242  
... ...