Commit 9a08012a6cbc70e94b8afdf256a1be3585f171ee
Merge branch 'integration' into devel
Showing
7 changed files
with
118 additions
and
49 deletions
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) | ... | ... |
NEWS
| 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 | ... | ... |