Commit 9a08012a6cbc70e94b8afdf256a1be3585f171ee

Authored by Jürgen Knödlseder
2 parents d6e2e852 ca35e885

Merge branch 'integration' into devel

ChangeLog
1   -2018-09-07
  1 +2018-09-10
2 2  
3 3 * Version 1.6.0 released
4 4 ========================
... ... @@ -9,6 +9,7 @@
9 9 Use full RMF true energy range for On/Off analysis in csspec (#2656)
10 10 Defer import of ROOT module in csroot2caldb to run method (#2652)
11 11 Transparently update parameter files (#2513)
  12 + Make cssrcdetect more robust (#2498)
12 13 Change formula for number of true energy bins in csphagen (#2490)
13 14 Remove ctools::is_valid_filename() method
14 15 Handle zero model for SUBDIV method in show_response.py (#2456)
... ...
1 1 New Features and Important Changes in ctools 1.6.0
2 2  
3   -7 September 2018
  3 +10 September 2018
4 4  
5 5  
6 6 Introduction
... ... @@ -335,7 +335,12 @@ certain amount (#2233).
335 335  
336 336 cssrcdetect - Detects sources in sky map
337 337 ----------------------------------------
338   -None
  338 +The script was made more robust by optionally convolving the input sky map
  339 +with a smoothing kernel with a specified correlation radius (options are a
  340 +"DISK" or a "GAUSSIAN" kernel, default is now "DISK"). In addition, the mean
  341 +and standard deviation of the pixel values are now computed locally over a
  342 +given radius, controlled through the "avgrad" parameter. This accomodates for
  343 +counts variations in the field of view (#2498).
339 344  
340 345  
341 346 cstsdist - Computes Test Statistic distribution
... ...
cscripts/cssrcdetect.par
... ... @@ -27,7 +27,10 @@ srcmodel, s, a, POINT,POINT,, "Source model type"
27 27 bkgmodel, s, a, NONE,NONE|IRF|AEFF|CUBE,, "Background model type"
28 28 threshold, r, a, 5.0,,, "Detection threshold (Gaussian sigma)"
29 29 maxsrcs, i, h, 20,1,1000, "Maximum number of sources"
30   -exclrad, r, h, 0.2,,, "Exclusion radius (deg)"
  30 +avgrad, r, h, 1.0,,, "Averaging radius (deg) for significance computation"
  31 +corr_rad, r, a, 0.1,,, "Correlation kernel radius (deg)"
  32 +corr_kern, s, h, DISK,NONE|DISK|GAUSSIAN,, "Correlation kernel to apply to input sky map"
  33 +exclrad, r, h, 0.3,,, "Exclusion radius (deg)"
31 34 fit_pos, b, h, yes,,, "Fit source positions?"
32 35 fit_shape, b, h, yes,,, "Fit source shapes?"
33 36  
... ...
cscripts/cssrcdetect.py
... ... @@ -51,6 +51,9 @@ class cssrcdetect(ctools.cscript):
51 51 else:
52 52 self._map = gammalib.GSkyMap()
53 53  
  54 + # Initialise other members
  55 + self._map_dirs = []
  56 +
54 57 # Return
55 58 return
56 59  
... ... @@ -73,6 +76,12 @@ class cssrcdetect(ctools.cscript):
73 76 self['fit_pos'].boolean()
74 77 self['fit_shape'].boolean()
75 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 +
76 85 # Query ahead output model filename
77 86 if self._read_ahead():
78 87 self['outmodel'].filename()
... ... @@ -100,20 +109,13 @@ class cssrcdetect(ctools.cscript):
100 109 self._log_header3(gammalib.NORMAL, 'Iteration '+str(i+1))
101 110  
102 111 # Get map moments
103   - mean, std = self._map_moments(counts)
  112 + mean, std = self._map_moments(counts, self['avgrad'].real())
104 113  
105   - # Log map moments
106   - self._log_value(gammalib.NORMAL, 'Map mean', mean)
107   - self._log_value(gammalib.NORMAL, 'Map standard deviation', std)
108   -
109 114 # Compute threshold
110   - threshold = mean + self['threshold'].real() * std
111   -
112   - # Log map threshold
113   - self._log_value(gammalib.NORMAL, 'Map threshold', threshold)
  115 + sigmap = (counts - mean)/std
114 116  
115 117 # Get maximum value and corresponding sky direction
116   - value, pos = self._find_maximum(counts, threshold=threshold)
  118 + value, pos = self._find_maximum(sigmap)
117 119  
118 120 # If maximum found then log maximum and add model
119 121 if pos is not None:
... ... @@ -124,12 +126,12 @@ class cssrcdetect(ctools.cscript):
124 126 # Log maximum
125 127 self._log_value(gammalib.NORMAL, 'Map maximum', str(value))
126 128 self._log_value(gammalib.NORMAL, name+' position', str(pos))
127   -
  129 +
128 130 # Add model
129 131 self._add_model(pos, name)
130 132  
131 133 # Remove maximum from map
132   - counts = self._remove_maximum(counts, pos, mean,
  134 + counts = self._remove_maximum(counts, pos, mean(pos),
133 135 radius=self['exclrad'].real())
134 136  
135 137 # ... otherwise log that no maximum was found and break iterations
... ... @@ -145,7 +147,7 @@ class cssrcdetect(ctools.cscript):
145 147 # Return
146 148 return
147 149  
148   - def _find_maximum(self, skymap, threshold=0.0):
  150 + def _find_maximum(self, skymap):
149 151 """
150 152 Find maximum in a sky map
151 153  
... ... @@ -153,8 +155,6 @@ class cssrcdetect(ctools.cscript):
153 155 ----------
154 156 skymap : `~gammalib.GSkyMap()`
155 157 Sky map
156   - threshold : float, optional
157   - Threshold for maximum value
158 158  
159 159 Returns
160 160 -------
... ... @@ -162,14 +162,14 @@ class cssrcdetect(ctools.cscript):
162 162 Maximum sky map value and corresponding sky direction
163 163 """
164 164 # Initialise maximum pixel value and sky direction
165   - value = threshold
  165 + value = self['threshold'].real()
166 166 pos = None
167 167  
168 168 # Loop over all pixels and find maximum
169 169 for i in range(skymap.npix()):
170 170 if skymap[i] > value:
171 171 value = skymap[i]
172   - pos = skymap.inx2dir(i)
  172 + pos = self._map_dirs[i]
173 173  
174 174 # Return sky direction of maximum
175 175 return value, pos
... ... @@ -196,17 +196,20 @@ class cssrcdetect(ctools.cscript):
196 196 """
197 197 # Copy skymap
198 198 skymap_copy = skymap.copy()
199   -
  199 +
  200 + # Cache the cosine of the radius
  201 + cos_radius = math.cos(math.radians(radius))
  202 +
200 203 # Loop over all pixels and find maximum
201 204 for i in range(skymap_copy.npix()):
202   - skymap_dir = skymap_copy.inx2dir(i)
203   - if skymap_dir.dist_deg(pos) < radius:
  205 + skymap_dir = self._map_dirs[i]
  206 + if skymap_dir.cos_dist(pos) > cos_radius:
204 207 skymap_copy[i] = value
205 208  
206 209 # Return copy of map
207 210 return skymap_copy
208 211  
209   - def _map_moments(self, skymap):
  212 + def _map_moments(self, skymap, radius):
210 213 """
211 214 Determine moments of sky map pixels
212 215  
... ... @@ -214,26 +217,26 @@ class cssrcdetect(ctools.cscript):
214 217 ----------
215 218 skymap : `~gammalib.GSkyMap()`
216 219 Sky map
  220 + radius : float
  221 + radius (deg) for pixel consideration
217 222  
218 223 Returns
219 224 -------
220   - mean, std : tuple of float
221   - Mean and standard deviation of pixel values
  225 + mean, std : tuple of GSkyMap
  226 + Mean and standard deviation of pixel values within a given radius
222 227 """
223   - # Initialise mean and standard deviation
224   - mean = 0.0
225   - std = 0.0
  228 + # Copy the input skymap
  229 + mean = gammalib.GSkyMap(skymap)
  230 + std = gammalib.GSkyMap(skymap)
  231 + std *= std
226 232  
227   - # Loop over all pixels
228   - for i in range(skymap.npix()):
229   - mean += skymap[i]
230   - std += skymap[i]*skymap[i]
  233 + # Convolve by disk to get bin-by-bin mean
  234 + mean.smooth('DISK', radius)
  235 + std.smooth('DISK', radius)
231 236  
232   - # Compute mean and standard deviation
233   - mean /= float(skymap.npix())
234   - std /= float(skymap.npix())
235   - std -= mean * mean
236   - std = math.sqrt(std)
  237 + # Compute the standard deviation for each pixel
  238 + std = std - (mean*mean)
  239 + std = std.sqrt()
237 240  
238 241 # Return mean and standard deviation
239 242 return mean, std
... ... @@ -338,6 +341,30 @@ class cssrcdetect(ctools.cscript):
338 341 return model
339 342  
340 343  
  344 + def _smooth_skymap(self, skymap):
  345 + """
  346 + Smooth the input sky map if a valid kernel was supplied
  347 +
  348 + Parameters
  349 + ----------
  350 + skymap : `~gammalib.GSkyMap()`
  351 + Sky map
  352 + """
  353 + # Log header
  354 + self._log_header3(gammalib.NORMAL, 'Smoothing Skymap')
  355 +
  356 + # Make sure the smoothing kernel is not 'NONE'
  357 + if self['corr_kern'].string().upper() != 'NONE':
  358 + self._log_value(gammalib.NORMAL, 'Kernel', self['corr_kern'].string())
  359 + self._log_value(gammalib.NORMAL, 'Parameter', self['corr_rad'].real())
  360 + skymap.smooth(self['corr_kern'].string(), self['corr_rad'].real())
  361 + else:
  362 + self._log_string(gammalib.NORMAL,
  363 + 'Smoothing kernel is "NONE", smoothing will be ignored.')
  364 +
  365 + return
  366 +
  367 +
341 368 def _load_skymap(self):
342 369 """
343 370 Load sky map
... ... @@ -360,7 +387,18 @@ class cssrcdetect(ctools.cscript):
360 387 fits.close()
361 388  
362 389 # Return
363   - return skymap
  390 + return skymap.extract(0)
  391 +
  392 +
  393 + def _cache_map_dirs(self, skymap):
  394 + """
  395 + Cache map pixel positions to save some computation time
  396 + """
  397 + # Setup the skymap directions
  398 + if not skymap.is_empty():
  399 + self._map_dirs = [skymap.inx2dir(i) for i in range(skymap.npix())]
  400 + else:
  401 + self._map_dirs = []
364 402  
365 403  
366 404 # Public methods
... ... @@ -378,13 +416,18 @@ class cssrcdetect(ctools.cscript):
378 416 # Get parameters
379 417 self._get_parameters()
380 418  
381   - # Write header
382   - self._log_header1(gammalib.NORMAL, 'Source detection')
383   -
384 419 # If sky map is empty then load it from input parameter
385 420 if self._map.is_empty():
386 421 self._map = self._load_skymap()
387 422  
  423 + self._cache_map_dirs(self._map)
  424 +
  425 + # Smooth the map
  426 + self._smooth_skymap(self._map)
  427 +
  428 + # Write header
  429 + self._log_header1(gammalib.NORMAL, 'Source detection')
  430 +
388 431 # Detect sources
389 432 self._detect_sources(self._map)
390 433  
... ...
doc/source/admin/release_history/1.6.rst
... ... @@ -43,6 +43,8 @@ Improvements
43 43 Use full ``RMF`` true energy range for On/Off analysis in :ref:`csspec`
44 44 * [`2513 <https://cta-redmine.irap.omp.eu/issues/2513>`_] -
45 45 Transparently update parameter files
  46 +* [`2498 <https://cta-redmine.irap.omp.eu/issues/2498>`_] -
  47 + Make :ref:`cssrcdetect` more robust
46 48 * [`2490 <https://cta-redmine.irap.omp.eu/issues/2490>`_] -
47 49 Change formula for number of true energy bins in :ref:`csphagen`
48 50 * [`2421 <https://cta-redmine.irap.omp.eu/issues/2421>`_] -
... ...
doc/source/users/reference_manual/cssrcdetect.rst
... ... @@ -51,6 +51,17 @@ General parameters
51 51 ``(maxsrcs = 20) [integer]``
52 52 Maximum number of sources that should be detected.
53 53  
  54 +``(avgrad = 1.0) [real]``
  55 + Averaging radius for significance computation (degrees). The script computes
  56 + the mean pixel value and standard deviation value over this radius and
  57 + uses the result to estimate the excess significance.
  58 +
  59 +``corr_rad [real]``
  60 + Correlation kernel radius (degrees).
  61 +
  62 +``(corr_kern = DISK) <NONE|DISK|GAUSSIAN> [string]``
  63 + Correlation kernel to use for smoothing the input sky map.
  64 +
54 65 ``(exclrad = 0.2) [real]``
55 66 Radius around a detected source that is excluded from further source
56 67 detection (degrees).
... ...
test/test_cssrcdetect.py
... ... @@ -33,7 +33,7 @@ class Test(test):
33 33 This test class makes unit tests for the cssrcdetect script by using it
34 34 from the command line and from Python.
35 35 """
36   -
  36 +
37 37 # Constructor
38 38 def __init__(self):
39 39 """
... ... @@ -75,7 +75,8 @@ class Test(test):
75 75 cmd = cssrcdetect+' inmap="'+self._map+'"'+ \
76 76 ' outmodel="cssrcdetect_cmd1.xml"'+ \
77 77 ' outds9file="cssrcdetect_cmd1.reg"'+ \
78   - ' srcmodel="POINT" bkgmodel="NONE" threshold=10.0'+ \
  78 + ' corr_rad=0.1'+ \
  79 + ' srcmodel="POINT" bkgmodel="NONE" threshold=8.0'+ \
79 80 ' logfile="cssrcdetect_cmd1.log" chatter=1'
80 81  
81 82 # Check if execution was successful
... ... @@ -89,7 +90,8 @@ class Test(test):
89 90 cmd = cssrcdetect+' inmap="skymap_that_does_not_exist"'+ \
90 91 ' outmodel="cssrcdetect_cmd1.xml"'+ \
91 92 ' outds9file="cssrcdetect_cmd1.reg"'+ \
92   - ' srcmodel="POINT" bkgmodel="NONE" threshold=10.0'+ \
  93 + ' corr_rad=0.1'+ \
  94 + ' srcmodel="POINT" bkgmodel="NONE" threshold=8.0'+ \
93 95 ' logfile="cssrcdetect_cmd2.log" debug=yes chatter=2'
94 96  
95 97 # Check if execution failed
... ... @@ -129,9 +131,10 @@ class Test(test):
129 131 srcdetect['outds9file'] = 'cssrcdetect_py1.reg'
130 132 srcdetect['srcmodel'] = 'POINT'
131 133 srcdetect['bkgmodel'] = 'NONE'
132   - srcdetect['threshold'] = 10.0
  134 + srcdetect['threshold'] = 8.0
  135 + srcdetect['corr_rad'] = 0.1
133 136 srcdetect['logfile'] = 'cssrcdetect_py1.log'
134   - srcdetect['chatter'] = 2
  137 + srcdetect['chatter'] = 4
135 138  
136 139 # Run cssrcdetect script and save models
137 140 srcdetect.logFileOpen() # Make sure we get a log file
... ... @@ -146,6 +149,7 @@ class Test(test):
146 149 srcdetect['outmodel'] = 'cssrcdetect_py2.xml'
147 150 srcdetect['outds9file'] = 'cssrcdetect_py2.reg'
148 151 srcdetect['bkgmodel'] = 'IRF'
  152 + srcdetect['threshold'] = 8.0
149 153 srcdetect['logfile'] = 'cssrcdetect_py2.log'
150 154 srcdetect['chatter'] = 3
151 155 srcdetect.logFileOpen() # Needed to get a new log file
... ... @@ -168,6 +172,6 @@ class Test(test):
168 172 # Check number of models
169 173 self.test_value(models.size(), number, 'Check for '+str(number)+
170 174 ' models in XML file')
171   -
  175 +
172 176 # Return
173 177 return
... ...