Coverage for python/lsst/pipe/tasks/setPrimaryFlags.py: 14%

92 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-30 01:39 -0800

1# This file is part of pipe_tasks. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

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 GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22__all__ = ["SetPrimaryFlagsConfig", "SetPrimaryFlagsTask"] 

23 

24import numpy as np 

25from lsst.pex.config import Config, Field, ListField 

26from lsst.pipe.base import Task 

27from lsst.geom import Box2D 

28 

29 

30def getPatchInner(sources, patchInfo): 

31 """Set a flag for each source if it is in the innerBBox of a patch. 

32 

33 Parameters 

34 ---------- 

35 sources : `lsst.afw.table.SourceCatalog` 

36 A sourceCatalog with pre-calculated centroids. 

37 patchInfo : `lsst.skymap.PatchInfo` 

38 Information about a `SkyMap` `Patch`. 

39 

40 Returns 

41 ------- 

42 isPatchInner : array-like of `bool` 

43 `True` for each source that has a centroid 

44 in the inner region of a patch. 

45 """ 

46 # Extract the centroid position for all the sources 

47 x = sources["slot_Centroid_x"] 

48 y = sources["slot_Centroid_y"] 

49 centroidFlag = sources["slot_Centroid_flag"] 

50 

51 # set inner flags for each source and set primary flags for 

52 # sources with no children (or all sources if deblend info not available) 

53 innerFloatBBox = Box2D(patchInfo.getInnerBBox()) 

54 inInner = innerFloatBBox.contains(x, y) 

55 

56 # When the centroider fails, we can still fall back to the peak, 

57 # but we don't trust that quite as much - 

58 # so we use a slightly smaller box for the patch comparison. 

59 shrunkInnerFloatBBox = Box2D(innerFloatBBox) 

60 shrunkInnerFloatBBox.grow(-1) 

61 inShrunkInner = shrunkInnerFloatBBox.contains(x, y) 

62 

63 # Flag sources contained in the inner region of a patch 

64 isPatchInner = (centroidFlag & inShrunkInner) | (~centroidFlag & inInner) 

65 return isPatchInner 

66 

67 

68def getTractInner(sources, tractInfo, skyMap): 

69 """Set a flag for each source that the skyMap includes in tractInfo. 

70 

71 Parameters 

72 ---------- 

73 sources : `lsst.afw.table.SourceCatalog` 

74 A sourceCatalog with pre-calculated centroids. 

75 tractInfo : `lsst.skymap.TractInfo` 

76 Tract object 

77 skyMap : `lsst.skymap.BaseSkyMap` 

78 Sky tessellation object 

79 

80 Returns 

81 ------- 

82 isTractInner : array-like of `bool` 

83 True if the skyMap.findTract method returns 

84 the same tract as tractInfo. 

85 """ 

86 tractId = tractInfo.getId() 

87 isTractInner = np.array([skyMap.findTract(s.getCoord()).getId() == tractId for s in sources]) 

88 return isTractInner 

89 

90 

91def getPseudoSources(sources, pseudoFilterList, schema, log): 

92 """Get a flag that marks pseudo sources. 

93 

94 Some categories of sources, for example sky objects, 

95 are not really detected sources and should not be considered primary 

96 sources. 

97 

98 Parameters 

99 ---------- 

100 sources : `lsst.afw.table.SourceCatalog` 

101 The catalog of sources for which to identify "pseudo" 

102 (e.g. sky) objects. 

103 pseudoFilterList : `list` of `str` 

104 Names of filters which should never be primary 

105 

106 Returns 

107 ------- 

108 isPseudo : array-like of `bool` 

109 True for each source that is a pseudo source. 

110 Note: to remove pseudo sources use `~isPseudo`. 

111 """ 

112 # Filter out sources that should never be primary 

113 isPseudo = np.zeros(len(sources), dtype=bool) 

114 for filt in pseudoFilterList: 

115 try: 

116 pseudoFilterKey = schema.find("merge_peak_%s" % filt).getKey() 

117 isPseudo |= sources[pseudoFilterKey] 

118 except KeyError: 

119 log.warning("merge_peak is not set for pseudo-filter %s", filt) 

120 return isPseudo 

121 

122 

123def getDeblendPrimaryFlags(sources): 

124 """Get flags generated by the deblender 

125 

126 scarlet is different than meas_deblender in that it is not 

127 (necessarily) flux conserving. For consistency in scarlet, 

128 all of the parents with only a single child (isolated sources) 

129 need to be deblended. This creates a question: which type 

130 of isolated source should we make measurements on, the 

131 undeblended "parent" or the deblended child? 

132 For that reason we distinguish between a DeblendedSource, 

133 which is a source that has no children and uses the 

134 isolated parents, and a DeblendedModelSource, which uses 

135 the scarlet models for both isolated and blended sources. 

136 In the case of meas_deblender, DeblendedModelSource is 

137 `None` because it is not contained in the output catalog. 

138 

139 Parameters 

140 ---------- 

141 sources : `lsst.afw.table.SourceCatalog` 

142 A sourceCatalog that has already been deblended using 

143 either meas_extensions_scarlet or meas_deblender. 

144 

145 Returns 

146 ------- 

147 fromBlend : array-like of `bool` 

148 True for each source modeled by the deblender from a `Peak` 

149 in a parent footprint that contained at least one other `Peak`. 

150 While these models can be approximated as isolated, 

151 and measurements are made on them as if that's the case, 

152 we know deblending to introduce biases in the shape and centroid 

153 of objects and it is important to know that the sources that these 

154 models are based on are all bleneded in the true image. 

155 isIsolated : array-like of `bool` 

156 True for isolated sources, regardless of whether or not they 

157 were modeled by the deblender. 

158 isDeblendedSource : array-like of `bool` 

159 True for each source that is a "DeblendedSource" as defined above. 

160 isDeblendedModelSource : array-like of `bool` 

161 True for each source that is a "DeblendedSourceModel" 

162 as defined above. 

163 """ 

164 nChildKey = "deblend_nChild" 

165 nChild = sources[nChildKey] 

166 parent = sources["parent"] 

167 

168 if "deblend_scarletFlux" in sources.schema: 

169 # The number of peaks in the sources footprint. 

170 # This (may be) different than nChild, 

171 # the number of deblended sources in the catalog, 

172 # because some peaks might have been culled during deblending. 

173 nPeaks = sources["deblend_nPeaks"] 

174 parentNChild = sources["deblend_parentNChild"] 

175 # It is possible for a catalog to contain a hierarchy of sources, 

176 # so we mark the leaves (end nodes of the hierarchy tree with no 

177 # children). 

178 isLeaf = nPeaks == 1 

179 fromBlend = parentNChild > 1 

180 isIsolated = isLeaf & ((parent == 0) | parentNChild == 1) 

181 isDeblendedSource = (fromBlend & isLeaf) | (isIsolated & (parent == 0)) 

182 isDeblendedModelSource = (parent != 0) & isLeaf 

183 else: 

184 # Set the flags for meas_deblender 

185 fromBlend = parent != 0 

186 isIsolated = (nChild == 0) & (parent == 0) 

187 isDeblendedSource = nChild == 0 

188 isDeblendedModelSource = None 

189 return fromBlend, isIsolated, isDeblendedSource, isDeblendedModelSource 

190 

191 

192class SetPrimaryFlagsConfig(Config): 

193 nChildKeyName = Field(dtype=str, default="deprecated", 

194 doc="Deprecated. This parameter is not used.") 

195 pseudoFilterList = ListField(dtype=str, default=['sky'], 

196 doc="Names of filters which should never be primary") 

197 

198 

199class SetPrimaryFlagsTask(Task): 

200 """Add isPrimaryKey to a given schema. 

201 

202 Parameters 

203 ---------- 

204 schema : `lsst.afw.table.Schema` 

205 The input schema. 

206 isSingleFrame : `bool` 

207 Flag specifying if task is operating with single frame imaging. 

208 includeDeblend : `bool` 

209 Include deblend information in isPrimary and 

210 add isDeblendedSource field? 

211 kwargs : 

212 Keyword arguments passed to the task. 

213 """ 

214 

215 ConfigClass = SetPrimaryFlagsConfig 

216 

217 def __init__(self, schema, isSingleFrame=False, **kwargs): 

218 Task.__init__(self, **kwargs) 

219 self.schema = schema 

220 self.isSingleFrame = isSingleFrame 

221 self.includeDeblend = False 

222 if not self.isSingleFrame: 

223 primaryDoc = ("true if source has no children and is in the inner region of a coadd patch " 

224 "and is in the inner region of a coadd tract " 

225 "and is not \"detected\" in a pseudo-filter (see config.pseudoFilterList)") 

226 self.isPatchInnerKey = self.schema.addField( 

227 "detect_isPatchInner", type="Flag", 

228 doc="true if source is in the inner region of a coadd patch", 

229 ) 

230 self.isTractInnerKey = self.schema.addField( 

231 "detect_isTractInner", type="Flag", 

232 doc="true if source is in the inner region of a coadd tract", 

233 ) 

234 else: 

235 primaryDoc = "true if source has no children and is not a sky source" 

236 self.isPrimaryKey = self.schema.addField( 

237 "detect_isPrimary", type="Flag", 

238 doc=primaryDoc, 

239 ) 

240 

241 if "deblend_nChild" in schema.getNames(): 

242 self.includeDeblend = True 

243 self.isDeblendedSourceKey = self.schema.addField( 

244 "detect_isDeblendedSource", type="Flag", 

245 doc=primaryDoc + " and is either an unblended isolated source or a " 

246 "deblended child from a parent with 'deblend_nChild' > 1") 

247 self.fromBlendKey = self.schema.addField( 

248 "detect_fromBlend", type="Flag", 

249 doc="This source is deblended from a parent with more than one child." 

250 ) 

251 self.isIsolatedKey = self.schema.addField( 

252 "detect_isIsolated", type="Flag", 

253 doc="This source is not a part of a blend." 

254 ) 

255 if "deblend_scarletFlux" in schema.getNames(): 

256 self.isDeblendedModelKey = self.schema.addField( 

257 "detect_isDeblendedModelSource", type="Flag", 

258 doc=primaryDoc + " and is a deblended child") 

259 else: 

260 self.isDeblendedModelKey = None 

261 

262 def run(self, sources, skyMap=None, tractInfo=None, patchInfo=None): 

263 """Set isPrimary and related flags on sources. 

264 

265 For coadded imaging, the `isPrimary` flag returns True when an object 

266 has no children, is in the inner region of a coadd patch, is in the 

267 inner region of a coadd trach, and is not detected in a pseudo-filter 

268 (e.g., a sky_object). 

269 For single frame imaging, the isPrimary flag returns True when a 

270 source has no children and is not a sky source. 

271 

272 Parameters 

273 ---------- 

274 sources : `lsst.afw.table.SourceCatalog` 

275 A sourceTable. Reads in centroid fields and an nChild field. 

276 Writes is-patch-inner, is-tract-inner, and is-primary flags. 

277 skyMap : `lsst.skymap.BaseSkyMap` 

278 Sky tessellation object 

279 tractInfo : `lsst.skymap.TractInfo` 

280 Tract object 

281 patchInfo : `lsst.skymap.PatchInfo` 

282 Patch object 

283 """ 

284 # Mark whether sources are contained within the inner regions of the 

285 # given tract/patch and are not "pseudo" (e.g. sky) sources. 

286 if not self.isSingleFrame: 

287 isPatchInner = getPatchInner(sources, patchInfo) 

288 isTractInner = getTractInner(sources, tractInfo, skyMap) 

289 isPseudo = getPseudoSources(sources, self.config.pseudoFilterList, self.schema, self.log) 

290 isPrimary = isTractInner & isPatchInner & ~isPseudo 

291 

292 sources[self.isPatchInnerKey] = isPatchInner 

293 sources[self.isTractInnerKey] = isTractInner 

294 else: 

295 # Mark all of the sky sources in SingleFrame images 

296 # (if they were added) 

297 if "sky_source" in sources.schema: 

298 isSky = sources["sky_source"] 

299 else: 

300 isSky = np.zeros(len(sources), dtype=bool) 

301 isPrimary = ~isSky 

302 

303 if self.includeDeblend: 

304 result = getDeblendPrimaryFlags(sources) 

305 fromBlend, isIsolated, isDeblendedSource, isDeblendedModelSource = result 

306 sources[self.fromBlendKey] = fromBlend 

307 sources[self.isIsolatedKey] = isIsolated 

308 sources[self.isDeblendedSourceKey] = isDeblendedSource 

309 if self.isDeblendedModelKey is not None: 

310 sources[self.isDeblendedModelKey] = isDeblendedModelSource 

311 isPrimary = isPrimary & isDeblendedSource 

312 

313 sources[self.isPrimaryKey] = isPrimary