ctools  2.0.0
 All Classes Namespaces Files Functions Variables Macros Pages
cssrcdetect.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # ==========================================================================
3 # Detects sources in a sky map
4 #
5 # Copyright (C) 2016-2022 Juergen Knoedlseder
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 math
23 import gammalib
24 import ctools
25 from cscripts import modutils
26 
27 
28 # ================= #
29 # cssrcdetect class #
30 # ================= #
31 class cssrcdetect(ctools.cscript):
32  """
33  Detects sources in a sky map
34  """
35 
36  # Constructor
37  def __init__(self, *argv):
38  """
39  Constructor
40  """
41  # Initialise sky map from constructor arguments
42  if len(argv) > 0 and isinstance(argv[0], gammalib.GSkyMap):
43  self._map = argv[0]
44  argv = argv[1:]
45  else:
46  self._map = gammalib.GSkyMap()
47 
48  # Initialise application by calling the base class constructor
49  self._init_cscript(self.__class__.__name__, ctools.__version__, argv)
50 
51  # Set protected members
52  self._models = gammalib.GModels()
53 
54  # Initialise other members
55  self._map_dirs = []
56 
57  # Return
58  return
59 
60 
61  # Private methods
62  def _get_parameters(self):
63  """
64  Get parameters from parfile
65  """
66  # Query input parameters if sky map is empty
67  if self._map.is_empty():
68  self['inmap'].filename()
69 
70  # Query further parameters
71  self['srcmodel'].string()
72  self['bkgmodel'].string()
73  self['threshold'].real()
74  self['maxsrcs'].integer()
75  self['exclrad'].real()
76  self['fit_pos'].boolean()
77  self['fit_shape'].boolean()
78 
79  # Query the smoothing parameters
80  self['avgrad'].real()
81  self['corr_kern'].string()
82  if self['corr_kern'].string().upper() != 'NONE':
83  self['corr_rad'].real()
84 
85  # Query ahead output model filename
86  if self._read_ahead():
87  self['outmodel'].filename()
88  self['outds9file'].filename()
89 
90  # Write input parameters into logger
91  self._log_parameters(gammalib.TERSE)
92 
93  # Return
94  return
95 
96  def _detect_sources(self, counts):
97  """
98  Detect sources in counts map
99 
100  Parameters
101  ----------
102  counts : `~gammalib.GSkyMap()`
103  Counts map
104  """
105  # Source detection iterations
106  for i in range(self['maxsrcs'].integer()):
107 
108  # Write header
109  self._log_header3(gammalib.NORMAL, 'Iteration '+str(i+1))
110 
111  # Get map moments
112  mean, std = self._map_moments(counts, self['avgrad'].real())
113 
114  # Compute threshold
115  sigmap = (counts - mean)/std
116 
117  # Get maximum value and corresponding sky direction
118  value, pos = self._find_maximum(sigmap)
119 
120  # If maximum found then log maximum and add model
121  if pos is not None:
122 
123  # Set source name
124  name = 'Src%3.3d' % (i+1)
125 
126  # Log maximum
127  self._log_value(gammalib.NORMAL, 'Map maximum', str(value))
128  self._log_value(gammalib.NORMAL, name+' position', str(pos))
129 
130  # Add model
131  self._add_model(pos, name)
132 
133  # Remove maximum from map
134  counts = self._remove_maximum(counts, pos, mean(pos),
135  radius=self['exclrad'].real())
136 
137  # ... otherwise log that no maximum was found and break iterations
138  else:
139 
140  # Log than no maximum was found
141  self._log_value(gammalib.NORMAL, 'Map maximum',
142  'None above threshold')
143 
144  # Break
145  break
146 
147  # Return
148  return
149 
150  def _find_maximum(self, skymap):
151  """
152  Find maximum in a sky map
153 
154  Parameters
155  ----------
156  skymap : `~gammalib.GSkyMap()`
157  Sky map
158 
159  Returns
160  -------
161  value, pos : tuple of float and `~gammalib.GSkyDir()`
162  Maximum sky map value and corresponding sky direction
163  """
164  # Initialise maximum pixel value and sky direction
165  value = self['threshold'].real()
166  pos = None
167 
168  # Loop over all pixels and find maximum
169  for i in range(skymap.npix()):
170  if skymap[i] > value:
171  value = skymap[i]
172  pos = self._map_dirs[i]
173 
174  # Return sky direction of maximum
175  return value, pos
176 
177  def _remove_maximum(self, skymap, pos, value=0.0, radius=0.1):
178  """
179  Remove maximum from sky map by replacing pixels with a given value
180 
181  Parameters
182  ----------
183  skymap : `~gammalib.GSkyMap()`
184  Sky map
185  pos : `~gammalib.GSkyDir()`
186  Sky direction of maximum
187  value : float, optional
188  Replacement value
189  radius : float, optional
190  Radius within which pixel values are replaced
191 
192  Returns
193  -------
194  skymap : `~gammalib.GSkyMap()`
195  Sky map with maximum removed
196  """
197  # Copy skymap
198  skymap_copy = skymap.copy()
199 
200  # Cache the cosine of the radius
201  cos_radius = math.cos(math.radians(radius))
202 
203  # Loop over all pixels and find maximum
204  for i in range(skymap_copy.npix()):
205  skymap_dir = self._map_dirs[i]
206  if skymap_dir.cos_dist(pos) > cos_radius:
207  skymap_copy[i] = value
208 
209  # Return copy of map
210  return skymap_copy
211 
212  def _map_moments(self, skymap, radius):
213  """
214  Determine moments of sky map pixels
215 
216  Parameters
217  ----------
218  skymap : `~gammalib.GSkyMap()`
219  Sky map
220  radius : float
221  radius (deg) for pixel consideration
222 
223  Returns
224  -------
225  mean, std : tuple of GSkyMap
226  Mean and standard deviation of pixel values within a given radius
227  """
228  # Copy the input skymap
229  mean = gammalib.GSkyMap(skymap)
230  std = gammalib.GSkyMap(skymap)
231  std *= std
232 
233  # Convolve by disk to get bin-by-bin mean
234  mean.smooth('DISK', radius)
235  std.smooth('DISK', radius)
236 
237  # Compute the squared of the standard deviation for each pixel
238  std = std - (mean*mean)
239 
240  # Compute minimum value of standard deviation
241  std_offset = 0.0
242  for index in range(std.npix()):
243  if std[index] < std_offset:
244  std_offset = std[index]
245 
246  # If minimum value is negative then subtract minimum value to guarantee
247  # a non-negative map of standard deviations
248  if std_offset < 0.0:
249  for index in range(std.npix()):
250  std[index] -= std_offset
251 
252  # Compute standard deviation
253  std = std.sqrt()
254 
255  # Return mean and standard deviation
256  return mean, std
257 
258  def _add_model(self, pos, name):
259  """
260  Add model to model container
261 
262  Parameters
263  ----------
264  pos : `~gammalib.GSkyDir()`
265  Sky direction of model
266  name : str
267  Model name
268  """
269  # Set point source model
270  model = self._set_ptsrc(pos)
271 
272  # Set model name
273  model.name(name)
274 
275  # Append model to container
276  self._models.append(model)
277 
278  # Return
279  return
280 
281  def _set_bkg(self, modeltype):
282  """
283  Set background model
284 
285  Parameters
286  ----------
287  modeltype : str
288  Model type ('IRF', 'AEFF', 'CUBE' or 'RACC')
289 
290  Returns
291  -------
292  model : `~gammalib.GModelData()`
293  Background model
294  """
295  # Pivot energy
296  epivot = gammalib.GEnergy(1.0, 'TeV')
297 
298  # Set background model
299  if modeltype == 'IRF':
300  spectral = gammalib.GModelSpectralPlaw(1.0, 0.0, epivot)
301  model = gammalib.GCTAModelIrfBackground(spectral)
302  elif modeltype == 'AEFF':
303  spectral = gammalib.GModelSpectralPlaw(1.0e-13, -2.5, epivot)
304  model = gammalib.GCTAModelAeffBackground(spectral)
305  elif modeltype == 'CUBE':
306  spectral = gammalib.GModelSpectralPlaw(1.0, 0.0, epivot)
307  model = gammalib.GCTAModelCubeBackground(spectral)
308  elif modeltype == 'RACC':
309  radial = gammalib.GCTAModelRadialGauss(3.0)
310  spectral = gammalib.GModelSpectralPlaw(1.0e-4, -2.5, epivot)
311  model = gammalib.GCTAModelRadialAcceptance(radial, spectral)
312  else:
313  model = None
314 
315  # Set background name
316  if model is not None:
317  model.name('Background')
318 
319  # Return model
320  return model
321 
322  def _set_ptsrc(self, pos):
323  """
324  Set point source model
325 
326  Parameters
327  ----------
328  pos : `~gammalib.GSkyDir()`
329  Sky direction of model
330 
331  Returns
332  -------
333  model : `~gammalib.GModelSky()`
334  Point source model
335  """
336  # Set spatial component
337  spatial = gammalib.GModelSpatialPointSource(pos)
338 
339  # Get fit parameters
340  fit_pos = self['fit_pos'].boolean()
341  #fit_shape = self['fit_shape'].boolean()
342 
343  # Loop over all parameters
344  for par in spatial:
345 
346  # Handle position parameters
347  if par.name() == 'RA' or par.name() == 'DEC':
348  if fit_pos:
349  par.free()
350  else:
351  par.fix()
352 
353  # Set spectral component (powerlaw with 1% of Crab)
354  spectral = gammalib.GModelSpectralPlaw(5.7e-18, -2.48,
355  gammalib.GEnergy(0.3, 'TeV'))
356 
357  # Set sky model
358  model = gammalib.GModelSky(spatial, spectral)
359 
360  # Return model
361  return model
362 
363 
364  def _smooth_skymap(self, skymap):
365  """
366  Smooth the input sky map if a valid kernel was supplied
367 
368  Parameters
369  ----------
370  skymap : `~gammalib.GSkyMap()`
371  Sky map
372  """
373  # Log header
374  self._log_header3(gammalib.NORMAL, 'Smoothing Skymap')
375 
376  # Make sure the smoothing kernel is not 'NONE'
377  if self['corr_kern'].string().upper() != 'NONE':
378  self._log_value(gammalib.NORMAL, 'Kernel', self['corr_kern'].string())
379  self._log_value(gammalib.NORMAL, 'Parameter', self['corr_rad'].real())
380  skymap.smooth(self['corr_kern'].string(), self['corr_rad'].real())
381  else:
382  self._log_string(gammalib.NORMAL,
383  'Smoothing kernel is "NONE", smoothing will be ignored.')
384 
385  return
386 
387 
388  def _load_skymap(self):
389  """
390  Load sky map
391 
392  Returns
393  -------
394  skymap : `~gammalib.GSkyMap()`
395  Sky map
396  """
397  # Get skymap filename
398  inmap = self['inmap'].filename()
399 
400  # Open sky map file
401  fits = gammalib.GFits(inmap)
402 
403  # Extract primary extension as sky map
404  skymap = gammalib.GSkyMap(fits.image(0))
405 
406  # Close sky map file
407  fits.close()
408 
409  # Return
410  return skymap.extract(0)
411 
412 
413  def _cache_map_dirs(self, skymap):
414  """
415  Cache map pixel positions to save some computation time
416  """
417  # Setup the skymap directions
418  if not skymap.is_empty():
419  self._map_dirs = [skymap.inx2dir(i) for i in range(skymap.npix())]
420  else:
421  self._map_dirs = []
422 
423 
424  # Public methods
425  def process(self):
426  """
427  Process the script
428  """
429  # Clear model container
430  self._models.clear()
431 
432  # Get parameters
433  self._get_parameters()
434 
435  # If sky map is empty then load it from input parameter
436  if self._map.is_empty():
437  self._map = self._load_skymap()
438 
439  self._cache_map_dirs(self._map)
440 
441  # Smooth the map
442  self._smooth_skymap(self._map)
443 
444  # Write header
445  self._log_header1(gammalib.NORMAL, 'Source detection')
446 
447  # Detect sources
448  self._detect_sources(self._map)
449 
450  # Write detected sources as models into logger
451  self._log_models(gammalib.NORMAL, self._models, 'Detected source model')
452 
453  # Write header
454  self._log_header1(gammalib.NORMAL, 'Add background model')
455 
456  # Add background model if requested
457  bkgmodel = self._set_bkg(self['bkgmodel'].string())
458  if bkgmodel is not None:
459 
460  # Append model
461  self._models.append(bkgmodel)
462 
463  # Log model
464  self._log_string(gammalib.NORMAL, str(bkgmodel))
465 
466  # ... otherwise notify that no background model is added
467  else:
468  self._log_value(gammalib.NORMAL, 'Background model', 'None')
469 
470  # Return
471  return
472 
473  def save(self):
474  """
475  Save sources into model definition XML file and ds9 file
476  """
477  # Write header
478  self._log_header1(gammalib.TERSE, 'Save sources')
479 
480  # Get output filenames
481  outmodel = self['outmodel'].filename()
482  outds9file = self['outds9file'].filename()
483 
484  # If model definition XML file exists and clobber flag is false then
485  # raise an exception
486  if outmodel.exists() and not self._clobber:
487  msg = ('Cannot save "'+outmodel.url()+'": File already exists. '
488  'Use parameter clobber=yes to allow overwriting of files.')
489  raise RuntimeError(msg)
490 
491  # If ds9 file exists and clobber flag is false then raise an exception
492  elif outds9file.exists() and not self._clobber:
493  msg = ('Cannot save "'+outds9file.url()+'": File already exists. '
494  'Use parameter clobber=yes to allow overwriting of files.')
495  raise RuntimeError(msg)
496 
497  # Otherwise log filename and save file
498  else:
499  # Log model definition XML filename
500  self._log_value(gammalib.NORMAL, 'Model definition XML file',
501  outmodel.url())
502 
503  # Save models
504  self._models.save(outmodel)
505 
506  # Log ds9 filename
507  self._log_value(gammalib.NORMAL, 'DS9 region file',
508  outds9file.url())
509 
510  # Save models as ds9 region file
511  modutils.models2ds9file(self._models, outds9file.url())
512 
513  # Return
514  return
515 
516  def models(self):
517  """
518  Return model container
519  """
520  return self._models
521 
522 
523 # ======================== #
524 # Main routine entry point #
525 # ======================== #
526 if __name__ == '__main__':
527 
528  # Create instance of application
529  app = cssrcdetect(sys.argv)
530 
531  # Execute application
532  app.execute()