ctools 2.1.0.dev
Loading...
Searching...
No Matches
cstsmapmerge.py
Go to the documentation of this file.
1#!/usr/bin/env python
2# ==========================================================================
3# Merge Test Statistic maps
4#
5# Copyright (C) 2015-2022 Michael Mayer
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 glob
23import os
24import gammalib
25import ctools
26
27
28# ================== #
29# cstsmapmerge class #
30# ================== #
31class cstsmapmerge(ctools.cscript):
32 """
33 Merge Test Statistic maps.
34 """
35
36 # Constructor
37 def __init__(self, *argv):
38 """
39 Constructor.
40 """
41 # Initialise application by calling the base class constructor
42 self._init_cscript(self.__class__.__name__, ctools.__version__, argv)
43
44 # Initialise class members
45 self._files = None
46 self._in_filename = ''
47 self._tsmap = gammalib.GSkyMap() # Empty sky map
48 self._statusmap = gammalib.GSkyMap() # Empty sky map
49 self._maps = []
50 self._mapnames = []
51 self._merged_files = []
52 self._overwrite = True
53 self._delete = False
54
55 # Return
56 return
57
58
59 # Private methods
60 def _get_parameters(self):
61 """
62 Get parameters from parfile.
63 """
64 # Get input maps string
65 inmaps = self['inmaps'].string()
66
67 # Handle ASCII files. If the file names given in the ASCII are
68 # relative filenames it is assumed that the filename is given
69 # relative to the location of the file.
70 if '@' == inmaps[0]:
71 filename = inmaps.replace('@','')
72 self._files = open(filename).read().splitlines()
73 dirname = os.path.dirname(filename)
74 files = []
75 for f in self._files:
76 if f[0] != '/':
77 fname = dirname + '/' + f
78 else:
79 fname = f
80 files.append(fname)
81 self._files = files
82
83 # Handle wild card strings
84 elif '*' in inmaps:
85 self._files = glob.glob(inmaps)
86
87 # Handle space-separated list
88 elif ' ' in inmaps:
89 self._files = inmaps.split(' ')
90
91 # Handle semi-colon separated list
92 elif ';' in inmaps:
93 self._files = inmaps.split(';')
94
95 # Throw exception if input models cannot be decoded
96 else:
97 msg = 'Parameter "inmaps" must contain either an @ASCII '\
98 'file, a semi-colon-separated or whitespace-separated '\
99 'list of files or a wildcard string.'
100 raise RuntimeError(msg)
101
102 # Check number of files. We need at least two files.
103 if len(self._files) <= 1:
104 msg = 'Need at least two files to start merging, '+\
105 str(len(self._files))+' file(s) given.'
106 raise RuntimeError(msg)
107
108 # Get other parameters
109 self._overwrite = self['overwrite'].boolean()
110 self._delete = self['delete'].boolean()
111
112 # Read ahead output filename
113 if self._read_ahead():
114 self['outmap'].filename()
115
116 # Write input parameters into logger
117 self._log_parameters(gammalib.TERSE)
118
119 # Return
120 return
121
122 def _init_ts_map(self, fitsfile):
123 """
124 Initialise Test Statistic map.
125 """
126 # Set filename
127 self._in_filename = fitsfile
128
129 # Open FITS file
130 fits = gammalib.GFits(fitsfile)
131
132 # Read TS and status maps
133 self._tsmap = gammalib.GSkyMap()
134 self._tsmap.read(fits[0])
135 self._statusmap = gammalib.GSkyMap()
136 self._statusmap.read(fits['STATUS MAP'])
137
138 # Get other maps
139 self._maps = []
140 self._mapnames = []
141
142 # Loop over extensions
143 for hdu in fits:
144
145 # Leave out primary and status extension
146 if hdu.extname() != 'IMAGE' and hdu.extname() != 'STATUS MAP':
147
148 # Add present maps
149 skymap = gammalib.GSkyMap()
150 skymap.read(hdu)
151 self._maps.append(skymap)
152 self._mapnames.append(hdu.extname())
153
154 # Close FITS file
155 fits.close()
156
157 # Return
158 return
159
160 def _merge_ts_map(self, fitsfile):
161 """
162 Merge TS map from FITS file into output TS map.
163
164 Args:
165 fitsfile: FITS file to be merged.
166 """
167 # Open FITS file
168 fits = gammalib.GFits(fitsfile)
169
170 # Read TS and status maps
171 add_tsmap = gammalib.GSkyMap()
172 add_tsmap.read(fits[0])
173 add_statusmap = gammalib.GSkyMap()
174 add_statusmap.read(fits['STATUS MAP'])
175
176 # Get other maps
177 add_maps = []
178
179 # Loop over extensions
180 for hdu in fits:
181
182 # Leave out primary and status extension
183 if hdu.extname() != 'IMAGE' and hdu.extname() != 'STATUS MAP':
184
185 # Add present maps
186 skymap = gammalib.GSkyMap()
187 skymap.read(hdu)
188 add_maps.append(skymap)
189
190 # Close FITS file
191 fits.close()
192
193 # Compare size of maps
194 if not len(add_maps) == len(self._maps):
195 msg = 'Cannot merge map "'+fitsfile+'" into map "'+\
196 self._in_filename+'" since the number of parameters ' +\
197 'between both maps is different.'
198 raise RuntimeError(msg)
199
200 # Loop over bins
201 for i in range(self._tsmap.npix()):
202
203 # Consider only bins that have been computed
204 if add_statusmap[i] > -0.5:
205
206 # Raise exception if this bin has already been computed
207 if self._statusmap[i] > -0.5 and not self._overwrite:
208 msg = 'Attempt to merge bin which apparently has '+\
209 'already been merged. File "'+fitsfile+'" '+\
210 'contains already merged bins. Set hidden '+\
211 'parameter "overwrite=yes" to avoid this error.'
212 raise RuntimeError(msg)
213
214 # Copy TS values
215 self._tsmap[i] = add_tsmap[i]
216
217 # Copy status
218 self._statusmap[i] = add_statusmap[i]
219
220 # Loop over maps and copy entries
221 for j in range(len(self._maps)):
222 self._maps[j][i] = add_maps[j][i]
223
224 # Return
225 return
226
227 def _get_number_of_ts_pixels(self, fitsfile):
228 """
229 Return number of pixels with TS values.
230
231 Args:
232 fitsfile: FITS file to be merged.
233
234 Returns:
235 Number of pixels for which TS has been computed.
236 """
237 # Get status map for this file
238 status = gammalib.GSkyMap(fitsfile+'[STATUS MAP]')
239
240 # Count number of pixels with TS status set
241 count = 0
242 for pix in status:
243 if pix > -0.5:
244 count += 1
245
246 # Return number of pixels
247 return count
248
249
250 # Public methods
251 def process(self):
252 """
253 Process the script.
254 """
255 # Get parameters
256 self._get_parameters()
257
258 # Write header into logger
259 self._log_header1(gammalib.TERSE, 'Merge TS maps')
260
261 # Initialise file to start with
262 file0 = ''
263 self._merged_files = []
264
265 # Test files for the entry status map. Use the first one to appear
266 # useful
267 for fitsfile in self._files:
268
269 # Skip file if it's not a FITS file
270 if not gammalib.GFilename(fitsfile).is_fits():
271
272 # Log message
273 self._log_value(gammalib.EXPLICIT, 'Skip file', fitsfile +
274 ' (not a FITS file)')
275
276 # Continue
277 continue
278
279 # Open FITS file
280 fits = gammalib.GFits(fitsfile)
281
282 # If file contains a status map then use it
283 if fits.contains('STATUS MAP'):
284
285 # Close FITS file
286 fits.close()
287
288 # Set filename to initial filename
289 file0 = fitsfile
290
291 # Log message
292 pix_info = ' (%d TS pixels computed)' % \
293 self._get_number_of_ts_pixels(fitsfile)
294 self._log_value(gammalib.TERSE, 'Initial TS map file',
295 fitsfile + pix_info)
296
297 # Leave loop
298 break
299
300 # ... otherwise signal that file is useless
301 else:
302
303 # Close fits file
304 fits.close()
305
306 # Info message
307 self._log_value(gammalib.EXPLICIT, 'Skip file', fitsfile +
308 ' (no "STATUS MAP" extension)')
309
310 # Continue
311 continue
312
313 # Signal if no suitable file was found
314 if file0 == '':
315 msg = 'None of the provided files seems to be a sliced ' + \
316 'TS map file (none has a "STATUS MAP" ' + \
317 'extension)'
318 raise RuntimeError(msg)
319
320 # ... otherwise merge files
321 else:
322
323 # Copy file list
324 workfiles = self._files
325
326 # Remove entry which will be used to initalise the map
327 workfiles.remove(file0)
328
329 # Initialise map from first file
330 self._init_ts_map(file0)
331
332 # Append to added files
333 self._merged_files.append(file0)
334
335 # Loop over files
336 for fitsfile in workfiles:
337
338 # Skip if file is not FITS
339 if not gammalib.GFilename(fitsfile).is_fits():
340 self._log_value(gammalib.EXPLICIT, 'Skip file', fitsfile +
341 ' (not a FITS file)')
342 continue
343
344 # Open FITS file
345 fits = gammalib.GFits(fitsfile)
346
347 # Skip if file does not contain status map
348 if not fits.contains('STATUS MAP'):
349 fits.close()
350 self._log_value(gammalib.EXPLICIT, 'Skip file', fitsfile +
351 ' (no "STATUS MAP" extension)')
352 continue
353
354 # Close FITS file
355 fits.close()
356
357 # Logging
358 pix_info = ' (%d TS pixels computed)' % \
359 self._get_number_of_ts_pixels(fitsfile)
360 self._log_value(gammalib.TERSE, 'Merge TS map file',
361 fitsfile + pix_info)
362
363 # Merge TS map
364 self._merge_ts_map(fitsfile)
365
366 # Append FITS file to merged files
367 self._merged_files.append(fitsfile)
368
369 # Return
370 return
371
372 def save(self):
373 """
374 Save TS map and remove slices if requested.
375 """
376
377 # Get output filename in case it was not read ahead
378 outmap = self['outmap'].filename()
379
380 # Create FITS file
381 fits = gammalib.GFits()
382
383 # Write TS map into primary
384 self._tsmap.write(fits)
385
386 # Loop over maps and write them to fits
387 for i in range(len(self._maps)):
388 self._maps[i].write(fits)
389
390 # Set map names as extensions
391 for i in range(len(self._mapnames)):
392 fits[i+1].extname(self._mapnames[i])
393
394 # Treat status map separately
395 self._statusmap.write(fits)
396 fits[fits.size()-1].extname('STATUS MAP')
397
398 # Stamp FITS file
399 self._stamp(fits)
400
401 # Check if map is fully done
402 bins_merged = 0
403 for pix in self._statusmap:
404 if pix > -0.5:
405 bins_merged += 1
406
407 # Set boolean if map is fully done
408 done = (bins_merged == self._tsmap.npix())
409
410 # Log summary header
411 self._log_header2(gammalib.TERSE, 'Merging Summary')
412
413 # Write computing status if we are not done yet
414 if not done:
415
416 # Log bins that were computed and those that were missing
417 self._log_value(gammalib.TERSE, 'TS map bins', self._tsmap.npix())
418 self._log_value(gammalib.TERSE, 'Bins computed and merged', bins_merged )
419 self._log_value(gammalib.TERSE, 'Bins missing', self._tsmap.npix() - bins_merged)
420
421 else:
422
423 # Write success message
424 self._log_string(gammalib.TERSE, 'TS map was fully computed and successfully merged')
425
426 # Write header
427 self._log_header1(gammalib.TERSE, 'Save TS map')
428
429 # Log filename
430 self._log_value(gammalib.TERSE, 'TS map file', outmap.url())
431
432 # Save FITS file
433 fits.saveto(outmap, self._clobber())
434
435 # Delete TS input maps if requested and map is fully done
436 if self._delete and done:
437
438 # Log header
439 self._log_header2(gammalib.TERSE, 'Delete slices TS map files')
440
441 # Loop over sliced files
442 for filename in self._merged_files:
443
444 # Remove file
445 os.remove(filename)
446
447 # Log message about file
448 self._log_value(gammalib.TERSE,'Deleted input file', filename)
449
450 # Return
451 return
452
453
454# ======================== #
455# Main routine entry point #
456# ======================== #
457if __name__ == '__main__':
458
459 # Create instance of application
460 app = cstsmapmerge(sys.argv)
461
462 # Execute application
463 app.execute()