ctools 2.1.0.dev
Loading...
Searching...
No Matches
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# ==========================================================================
21import sys
22import math
23import gammalib
24import ctools
25from cscripts import modutils
26
27
28# ================= #
29# cssrcdetect class #
30# ================= #
31class 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# ======================== #
526if __name__ == '__main__':
527
528 # Create instance of application
529 app = cssrcdetect(sys.argv)
530
531 # Execute application
532 app.execute()
_map_moments(self, skymap, radius)
_remove_maximum(self, skymap, pos, value=0.0, radius=0.1)