Coverage for python/lsst/meas/algorithms/astrometrySourceSelector.py: 33%

67 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-23 10:17 +0000

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <https://www.lsstcorp.org/LegalNotices/>. 

22# 

23"""Select sources that are useful for astrometry. 

24 

25Such sources have good signal-to-noise, are well centroided, not blended, 

26and not flagged with a handful of "bad" flags. 

27""" 

28 

29__all__ = ["AstrometrySourceSelectorConfig", "AstrometrySourceSelectorTask"] 

30 

31import numpy as np 

32 

33import lsst.pex.config as pexConfig 

34from lsst.pex.exceptions import NotFoundError 

35from .sourceSelector import BaseSourceSelectorConfig, BaseSourceSelectorTask, sourceSelectorRegistry 

36from lsst.pipe.base import Struct 

37from functools import reduce 

38 

39 

40class AstrometrySourceSelectorConfig(BaseSourceSelectorConfig): 

41 badFlags = pexConfig.ListField( 

42 doc="List of flags which cause a source to be rejected as bad", 

43 dtype=str, 

44 default=[ 

45 "base_PixelFlags_flag_edge", 

46 "base_PixelFlags_flag_interpolatedCenter", 

47 "base_PixelFlags_flag_saturatedCenter", 

48 "base_PixelFlags_flag_crCenter", 

49 "base_PixelFlags_flag_bad", 

50 ], 

51 ) 

52 sourceFluxType = pexConfig.Field( 

53 doc="Type of source flux; typically one of Ap or Psf", 

54 dtype=str, 

55 default="Ap", 

56 ) 

57 minSnr = pexConfig.Field( 

58 dtype=float, 

59 doc="Minimum allowed signal-to-noise ratio for sources used for matching " 

60 "(in the flux specified by sourceFluxType); <= 0 for no limit", 

61 default=10, 

62 ) 

63 

64 

65@pexConfig.registerConfigurable("astrometry", sourceSelectorRegistry) 

66class AstrometrySourceSelectorTask(BaseSourceSelectorTask): 

67 """Select sources that are useful for astrometry. 

68 

69 Good astrometry sources have high signal/noise, are non-blended, and 

70 did not have certain "bad" flags set during source extraction. They need not 

71 be PSF sources, just have reliable centroids. 

72 """ 

73 ConfigClass = AstrometrySourceSelectorConfig 

74 

75 def __init__(self, *args, **kwargs): 

76 BaseSourceSelectorTask.__init__(self, *args, **kwargs) 

77 

78 def selectSources(self, sourceCat, matches=None, exposure=None): 

79 """Return a selection of sources that are useful for astrometry. 

80 

81 Parameters 

82 ---------- 

83 sourceCat : `lsst.afw.table.SourceCatalog` 

84 Catalog of sources to select from. 

85 This catalog must be contiguous in memory. 

86 matches : `list` of `lsst.afw.table.ReferenceMatch` or None 

87 Ignored in this SourceSelector. 

88 exposure : `lsst.afw.image.Exposure` or None 

89 The exposure the catalog was built from; used for debug display. 

90 

91 Returns 

92 ------- 

93 struct : `lsst.pipe.base.Struct` 

94 The struct contains the following data: 

95 

96 ``selected`` 

97 Boolean array of sources that were selected, same length as 

98 sourceCat. (`numpy.ndarray` of `bool`) 

99 """ 

100 self._getSchemaKeys(sourceCat.schema) 

101 

102 bad = reduce(lambda x, y: np.logical_or(x, sourceCat.get(y)), self.config.badFlags, False) 

103 good = self._isGood(sourceCat) 

104 return Struct(selected=good & ~bad) 

105 

106 def _getSchemaKeys(self, schema): 

107 """Extract and save the necessary keys from schema with asKey. 

108 """ 

109 self.parentKey = schema["parent"].asKey() 

110 self.nChildKey = schema["deblend_nChild"].asKey() 

111 self.centroidXKey = schema["slot_Centroid_x"].asKey() 

112 self.centroidYKey = schema["slot_Centroid_y"].asKey() 

113 self.centroidXErrKey = schema["slot_Centroid_xErr"].asKey() 

114 self.centroidYErrKey = schema["slot_Centroid_yErr"].asKey() 

115 self.centroidFlagKey = schema["slot_Centroid_flag"].asKey() 

116 try: 

117 self.primaryKey = schema["detect_isPrimary"].asKey() 

118 except NotFoundError: 

119 self.primaryKey = None 

120 

121 self.edgeKey = schema["base_PixelFlags_flag_edge"].asKey() 

122 self.interpolatedCenterKey = schema["base_PixelFlags_flag_interpolatedCenter"].asKey() 

123 self.saturatedKey = schema["base_PixelFlags_flag_saturated"].asKey() 

124 

125 fluxPrefix = "slot_%sFlux_" % (self.config.sourceFluxType,) 

126 self.instFluxKey = schema[fluxPrefix + "instFlux"].asKey() 

127 self.fluxFlagKey = schema[fluxPrefix + "flag"].asKey() 

128 self.instFluxErrKey = schema[fluxPrefix + "instFluxErr"].asKey() 

129 

130 def _isMultiple(self, sourceCat): 

131 """Return True for each source that is likely multiple sources. 

132 """ 

133 test = (sourceCat.get(self.parentKey) != 0) | (sourceCat.get(self.nChildKey) != 0) 

134 # have to currently manage footprints on a source-by-source basis. 

135 for i, cat in enumerate(sourceCat): 

136 footprint = cat.getFootprint() 

137 test[i] |= (footprint is not None) and (len(footprint.getPeaks()) > 1) 

138 return test 

139 

140 def _hasCentroid(self, sourceCat): 

141 """Return True for each source that has a valid centroid 

142 """ 

143 def checkNonfiniteCentroid(): 

144 """Return True for sources with non-finite centroids. 

145 """ 

146 return ~np.isfinite(sourceCat.get(self.centroidXKey)) | \ 

147 ~np.isfinite(sourceCat.get(self.centroidYKey)) 

148 assert ~checkNonfiniteCentroid().any(), \ 

149 "Centroids not finite for %d unflagged sources." % (checkNonfiniteCentroid().sum()) 

150 return np.isfinite(sourceCat.get(self.centroidXErrKey)) \ 

151 & np.isfinite(sourceCat.get(self.centroidYErrKey)) \ 

152 & ~sourceCat.get(self.centroidFlagKey) 

153 

154 def _goodSN(self, sourceCat): 

155 """Return True for each source that has Signal/Noise > config.minSnr. 

156 """ 

157 if self.config.minSnr <= 0: 

158 return True 

159 else: 

160 with np.errstate(invalid="ignore"): # suppress NAN warnings 

161 return sourceCat.get(self.instFluxKey)/sourceCat.get(self.instFluxErrKey) > self.config.minSnr 

162 

163 def _isUsable(self, sourceCat): 

164 """Return True for each source that is usable for matching, even if it may 

165 have a poor centroid. 

166 

167 For a source to be usable it must: 

168 - have a valid centroid 

169 - not be deblended 

170 - have a valid flux (of the type specified in this object's constructor) 

171 - have adequate signal-to-noise 

172 """ 

173 

174 return self._hasCentroid(sourceCat) \ 

175 & ~self._isMultiple(sourceCat) \ 

176 & self._goodSN(sourceCat) \ 

177 & ~sourceCat.get(self.fluxFlagKey) 

178 

179 def _isPrimary(self, sourceCat): 

180 """Return True if this is a primary source. 

181 """ 

182 if self.primaryKey: 

183 return sourceCat.get(self.primaryKey) 

184 else: 

185 return np.ones(len(sourceCat), dtype=bool) 

186 

187 def _isGood(self, sourceCat): 

188 """Return True for each source that is usable for matching and likely has a 

189 good centroid. 

190 

191 The additional tests for a good centroid, beyond isUsable, are: 

192 - not interpolated in the center 

193 - not saturated 

194 - not near the edge 

195 """ 

196 

197 return self._isUsable(sourceCat) \ 

198 & self._isPrimary(sourceCat) \ 

199 & ~sourceCat.get(self.saturatedKey) \ 

200 & ~sourceCat.get(self.interpolatedCenterKey) \ 

201 & ~sourceCat.get(self.edgeKey) 

202 

203 def _isBadFlagged(self, source): 

204 """Return True if any of config.badFlags are set for this source. 

205 """ 

206 return any(source.get(flag) for flag in self.config.badFlags)