Coverage for python/lsst/meas/algorithms/setPrimaryFlags.py: 14%

92 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-30 10:19 +0000

1# This file is part of meas_algorithms. 

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 

25 

26from lsst.pex.config import Config, ListField 

27from lsst.pipe.base import Task 

28from lsst.geom import Box2D 

29 

30 

31class SetPrimaryFlagsConfig(Config): 

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

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

34 

35 

36class SetPrimaryFlagsTask(Task): 

37 """Set the ``isPrimary`` flag and either various blendedness, or 

38 patch/tract flags to a catalog (for single frame or coadd catalogs, 

39 respectively), based on other properties of the sources. 

40 

41 Parameters 

42 ---------- 

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

44 Source catalog schema to add fields to. 

45 isSingleFrame : `bool` 

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

47 includeDeblend : `bool` 

48 Include deblend information in isPrimary and add blendedness fields? 

49 

50 Notes 

51 ----- 

52 The tests for this task still live in 

53 ``pipe_tasks/tests/test_isPrimaryFlag.py``; see discussion on DM-42720. 

54 """ 

55 

56 _DefaultName = "setPrimaryFlags" 

57 ConfigClass = SetPrimaryFlagsConfig 

58 

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

60 super().__init__(**kwargs) 

61 self.schema = schema 

62 self.isSingleFrame = isSingleFrame 

63 self.includeDeblend = False 

64 if not self.isSingleFrame: 

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

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

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

68 self.isPatchInnerKey = self.schema.addField( 

69 "detect_isPatchInner", type="Flag", 

70 doc="True if source is in the inner region of a coadd patch", 

71 ) 

72 self.isTractInnerKey = self.schema.addField( 

73 "detect_isTractInner", type="Flag", 

74 doc="True if source is in the inner region of a coadd tract", 

75 ) 

76 else: 

77 primaryDoc = "True if source has no children and is not a sky source." 

78 self.isPrimaryKey = self.schema.addField( 

79 "detect_isPrimary", type="Flag", 

80 doc=primaryDoc, 

81 ) 

82 

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

84 self.includeDeblend = True 

85 self.isDeblendedSourceKey = self.schema.addField( 

86 "detect_isDeblendedSource", type="Flag", 

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

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

89 self.fromBlendKey = self.schema.addField( 

90 "detect_fromBlend", type="Flag", 

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

92 ) 

93 self.isIsolatedKey = self.schema.addField( 

94 "detect_isIsolated", type="Flag", 

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

96 ) 

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

98 self.isDeblendedModelKey = self.schema.addField( 

99 "detect_isDeblendedModelSource", type="Flag", 

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

101 else: 

102 self.isDeblendedModelKey = None 

103 

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

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

106 

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

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

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

110 (e.g., a sky_object). 

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

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

113 

114 Parameters 

115 ---------- 

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

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

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

119 skyMap : `lsst.skymap.BaseSkyMap` 

120 Sky tessellation object 

121 tractInfo : `lsst.skymap.TractInfo`, optional 

122 Tract object; required if ``self.isSingleFrame`` is False. 

123 patchInfo : `lsst.skymap.PatchInfo` 

124 Patch object; required if ``self.isSingleFrame`` is False. 

125 """ 

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

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

128 if not self.isSingleFrame: 

129 isPatchInner = getPatchInner(sources, patchInfo) 

130 isTractInner = getTractInner(sources, tractInfo, skyMap) 

131 isPseudo = self._getPseudoSources(sources) 

132 isPrimary = isTractInner & isPatchInner & ~isPseudo 

133 

134 sources[self.isPatchInnerKey] = isPatchInner 

135 sources[self.isTractInnerKey] = isTractInner 

136 else: 

137 # Mark all of the sky sources in SingleFrame images 

138 # (if they were added) 

139 if "sky_source" in sources.schema: 

140 isSky = sources["sky_source"] 

141 else: 

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

143 isPrimary = ~isSky 

144 

145 if self.includeDeblend: 

146 result = getDeblendPrimaryFlags(sources) 

147 fromBlend, isIsolated, isDeblendedSource, isDeblendedModelSource = result 

148 sources[self.fromBlendKey] = fromBlend 

149 sources[self.isIsolatedKey] = isIsolated 

150 sources[self.isDeblendedSourceKey] = isDeblendedSource 

151 if self.isDeblendedModelKey is not None: 

152 sources[self.isDeblendedModelKey] = isDeblendedModelSource 

153 isPrimary = isPrimary & isDeblendedSource 

154 

155 sources[self.isPrimaryKey] = isPrimary 

156 

157 def _getPseudoSources(self, sources): 

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

159 

160 Some categories of sources, for example sky objects, are not really 

161 detected sources and should not be considered primary sources. 

162 

163 Parameters 

164 ---------- 

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

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

167 (e.g. sky) objects. 

168 

169 Returns 

170 ------- 

171 isPseudo : array-like of `bool` 

172 True for each source that is a pseudo source. 

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

174 """ 

175 # Filter out sources that should never be primary. 

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

177 for filt in self.config.pseudoFilterList: 

178 try: 

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

180 isPseudo |= sources[pseudoFilterKey] 

181 except KeyError: 

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

183 return isPseudo 

184 

185 

186def getPatchInner(sources, patchInfo): 

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

188 

189 Parameters 

190 ---------- 

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

192 A sourceCatalog with pre-calculated centroids. 

193 patchInfo : `lsst.skymap.PatchInfo` 

194 Information about a `SkyMap` `Patch`. 

195 

196 Returns 

197 ------- 

198 isPatchInner : array-like of `bool` 

199 `True` for each source that has a centroid 

200 in the inner region of a patch. 

201 """ 

202 # Extract the centroid position for all the sources 

203 x = sources["slot_Centroid_x"] 

204 y = sources["slot_Centroid_y"] 

205 centroidFlag = sources["slot_Centroid_flag"] 

206 

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

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

209 innerFloatBBox = Box2D(patchInfo.getInnerBBox()) 

210 inInner = innerFloatBBox.contains(x, y) 

211 

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

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

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

215 shrunkInnerFloatBBox = Box2D(innerFloatBBox) 

216 shrunkInnerFloatBBox.grow(-1) 

217 inShrunkInner = shrunkInnerFloatBBox.contains(x, y) 

218 

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

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

221 return isPatchInner 

222 

223 

224def getTractInner(sources, tractInfo, skyMap): 

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

226 

227 Parameters 

228 ---------- 

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

230 A sourceCatalog with pre-calculated centroids. 

231 tractInfo : `lsst.skymap.TractInfo` 

232 Tract object 

233 skyMap : `lsst.skymap.BaseSkyMap` 

234 Sky tessellation object 

235 

236 Returns 

237 ------- 

238 isTractInner : array-like of `bool` 

239 True if the skyMap.findTract method returns 

240 the same tract as tractInfo. 

241 """ 

242 tractId = tractInfo.getId() 

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

244 return isTractInner 

245 

246 

247def getDeblendPrimaryFlags(sources): 

248 """Get flags generated by the deblender. 

249 

250 scarlet is different than meas_deblender in that it is not 

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

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

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

254 of isolated source should we make measurements on, the 

255 undeblended "parent" or the deblended child? 

256 For that reason we distinguish between a DeblendedSource, 

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

258 isolated parents, and a DeblendedModelSource, which uses 

259 the scarlet models for both isolated and blended sources. 

260 In the case of meas_deblender, DeblendedModelSource is 

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

262 

263 Parameters 

264 ---------- 

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

266 A sourceCatalog that has already been deblended using 

267 either meas_extensions_scarlet or meas_deblender. 

268 

269 Returns 

270 ------- 

271 fromBlend : array-like of `bool` 

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

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

274 While these models can be approximated as isolated, 

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

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

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

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

279 isIsolated : array-like of `bool` 

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

281 were modeled by the deblender. 

282 isDeblendedSource : array-like of `bool` 

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

284 isDeblendedModelSource : array-like of `bool` 

285 True for each source that is a "DeblendedSourceModel" 

286 as defined above. 

287 """ 

288 nChildKey = "deblend_nChild" 

289 nChild = sources[nChildKey] 

290 parent = sources["parent"] 

291 

292 if "deblend_scarletFlux" in sources.schema: 

293 # The number of peaks in the sources footprint. 

294 # This (may be) different than nChild, 

295 # the number of deblended sources in the catalog, 

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

297 nPeaks = sources["deblend_nPeaks"] 

298 parentNChild = sources["deblend_parentNChild"] 

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

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

301 # children). 

302 isLeaf = nPeaks == 1 

303 fromBlend = parentNChild > 1 

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

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

306 isDeblendedModelSource = (parent != 0) & isLeaf 

307 else: 

308 # Set the flags for meas_deblender 

309 fromBlend = parent != 0 

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

311 isDeblendedSource = nChild == 0 

312 isDeblendedModelSource = None 

313 return fromBlend, isIsolated, isDeblendedSource, isDeblendedModelSource