ctools  2.0.0
 All Classes Namespaces Files Functions Variables Macros Pages
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 # ==========================================================================
21 import sys
22 import glob
23 import os
24 import gammalib
25 import ctools
26 
27 
28 # ================== #
29 # cstsmapmerge class #
30 # ================== #
31 class 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 # ======================== #
457 if __name__ == '__main__':
458 
459  # Create instance of application
460  app = cstsmapmerge(sys.argv)
461 
462  # Execute application
463  app.execute()