Commit 31bf170bf81b7e261e061c3a56e9d9b14f609375
1 parent
d5075297
Add inmap parameter to comlixmap (#4187)
Showing
7 changed files
with
209 additions
and
34 deletions
ChangeLog
NEWS
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 | ... | ... |