Coverage for tests / test_filterDiaSourceCatalog.py: 12%

200 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-14 00:44 -0700

1# This file is part of ap_association. 

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 

22import unittest 

23import numpy as np 

24 

25from lsst.ap.association.filterDiaSourceCatalog import (FilterDiaSourceCatalogConfig, 

26 FilterDiaSourceCatalogTask, 

27 FilterDiaSourceReliabilityConfig, 

28 FilterDiaSourceReliabilityTask) 

29import lsst.geom as geom 

30import lsst.meas.base.tests as measTests 

31import lsst.utils.tests 

32import lsst.afw.image as afwImage 

33import lsst.afw.table as afwTable 

34import lsst.daf.base as dafBase 

35 

36 

37class TestFilterDiaSourceCatalogTask(unittest.TestCase): 

38 

39 def setUp(self): 

40 self.config = FilterDiaSourceCatalogConfig() 

41 

42 self.nSkySources = 5 

43 self.nCrCenterSources = 6 

44 self.nFakeFlagSources = 4 

45 self.nNegativeSources = 7 

46 self.nTrailedSources = 10 

47 self.pixelScale = 0.2 # arcseconds/pixel 

48 self.nSources = (self.nSkySources + self.nCrCenterSources + self.nFakeFlagSources 

49 + self.nNegativeSources + self.nTrailedSources) 

50 self.yLoc = 100 

51 self.expId = 4321 

52 self.bbox = geom.Box2I(geom.Point2I(0, 0), 

53 geom.Extent2I(1024, 1153)) 

54 dataset = measTests.TestDataset(self.bbox) 

55 for srcIdx in range(self.nSources): 

56 dataset.addSource(10000.0, geom.Point2D(srcIdx, self.yLoc)) 

57 schema = dataset.makeMinimalSchema() 

58 schema.addField("sky_source", type="Flag", doc="Sky objects.") 

59 schema.addField("base_PixelFlags_flag_crCenter", type="Flag", doc="A cosmic ray was detected " 

60 "and interpolated in this object's center.") 

61 schema.addField("base_PixelFlags_flag_high_varianceCenterAll", type="Flag", doc="The object was " 

62 "detected in a region with exceptionally high template variance.") 

63 schema.addField("fakeBadFlag", type="Flag", doc="A fake flag to test a badFlagList longer " 

64 "than one item.") 

65 schema.addField("ip_diffim_forced_PsfFlux_instFlux", type="F", 

66 doc="Forced photometry flux for a point source model measured on the visit image " 

67 "centered at DiaSource position.") 

68 schema.addField("ip_diffim_forced_PsfFlux_instFluxErr", type="F", 

69 doc="Estimated uncertainty of ip_diffim_forced_PsfFlux_instFlux.") 

70 schema.addField('ext_trailedSources_Naive_flag_off_image', type="Flag", 

71 doc="Trail extends off image") 

72 schema.addField('ext_trailedSources_Naive_flag_suspect_long_trail', 

73 type="Flag", doc="Trail length is greater than three times the psf radius") 

74 schema.addField('ext_trailedSources_Naive_flag_edge', type="Flag", 

75 doc="Trail contains edge pixels") 

76 schema.addField('ext_trailedSources_Naive_flag_nan', type="Flag", 

77 doc="One or more trail coordinates are missing") 

78 schema.addField('ext_trailedSources_Naive_length', type="F", 

79 doc="Length of the source trail") 

80 schema.addField("reliability", type="F", 

81 doc="Reliability of the source") 

82 _, self.diaSourceCat = dataset.realize(10.0, schema, randomSeed=1234) 

83 

84 # set the sky_source flag for the first set 

85 self.diaSourceCat[0:self.nSkySources]["sky_source"] = True 

86 

87 # set the pixelFlags_crCenter flag 

88 crCenter_offset = self.nSkySources 

89 self.diaSourceCat[crCenter_offset:crCenter_offset+self.nCrCenterSources][ 

90 "base_PixelFlags_flag_crCenter" 

91 ] = True 

92 

93 # set the fakeBadFlag flag 

94 fakeFlag_offset = crCenter_offset + self.nCrCenterSources 

95 self.diaSourceCat[fakeFlag_offset:fakeFlag_offset+self.nFakeFlagSources][ 

96 "fakeBadFlag" 

97 ] = True 

98 

99 # create increasingly negative ip_diffim_forced_PsfFlux_instFlux/ip_diffim_forced_PsfFlux_instFluxErr 

100 self.nRemovedNegativeSources = 0 

101 negativeSources_offset = fakeFlag_offset + self.nFakeFlagSources 

102 for i, srcIdx in enumerate(range(negativeSources_offset, 

103 negativeSources_offset+self.nNegativeSources)): 

104 self.diaSourceCat[srcIdx]["ip_diffim_forced_PsfFlux_instFlux"] = -0.5 * i 

105 self.diaSourceCat[srcIdx]["ip_diffim_forced_PsfFlux_instFluxErr"] = 1.01 

106 if (-0.5 * i)/1.01 < self.config.minAllowedDirectSnr: 

107 self.nRemovedNegativeSources += 1 

108 

109 # The last 10 sources will all contained trail length measurements, 

110 # increasing in size by 1.5 arcseconds. Only the last three will have 

111 # lengths which are too long and will be filtered out. 

112 self.nFilteredTrailedSources = 0 

113 trail_offset = negativeSources_offset + self.nNegativeSources 

114 for i, srcIdx in enumerate(range(trail_offset, trail_offset+self.nTrailedSources)): 

115 self.diaSourceCat[srcIdx]["ext_trailedSources_Naive_length"] = 1.5*(i+1)/self.pixelScale 

116 if 1.5*(i+1) > 36000/3600.0/24.0 * 30.0: 

117 self.nFilteredTrailedSources += 1 

118 # Setting a combination of flags for filtering in tests 

119 self.diaSourceCat[trail_offset+1]["ext_trailedSources_Naive_flag_off_image"] = True 

120 self.diaSourceCat[trail_offset+2]["ext_trailedSources_Naive_flag_suspect_long_trail"] = True 

121 self.diaSourceCat[trail_offset+2]["ext_trailedSources_Naive_flag_edge"] = True 

122 # As only two of these flags are set, the total number of filtered 

123 # sources will be self.nFilteredTrailedSources + 2 

124 self.nFilteredTrailedSources += 2 

125 

126 mjd = 57071.0 

127 self.utc_jd = mjd + 2_400_000.5 - 35.0 / (24.0 * 60.0 * 60.0) 

128 

129 self.visitInfo = afwImage.VisitInfo( 

130 # This incomplete visitInfo is sufficient for testing because the 

131 # Python constructor sets all other required values to some 

132 # default. 

133 exposureTime=30.0, 

134 darkTime=3.0, 

135 date=dafBase.DateTime(mjd, system=dafBase.DateTime.MJD), 

136 boresightRaDec=geom.SpherePoint(0.0, 0.0, geom.degrees), 

137 ) 

138 

139 def test_run_without_filter(self): 

140 """Test that when all filters are turned off all sources in the catalog 

141 are returned. 

142 """ 

143 self.config.doRemoveSkySources = False 

144 self.config.badFlagList = [] 

145 self.config.doRemoveNegativeDirectImageSources = False 

146 self.config.doWriteRejectedSkySources = False 

147 self.config.doTrailedSourceFilter = False 

148 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

149 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

150 self.assertEqual(len(result.filteredDiaSourceCat), len(self.diaSourceCat)) 

151 self.assertEqual(len(result.rejectedDiaSources), 0) 

152 self.assertEqual(len(self.diaSourceCat), self.nSources) 

153 

154 def test_run_with_filter_sky_only(self): 

155 """Test that when only the sky filter is turned on the first five 

156 sources which are flagged as sky objects are filtered out of the 

157 catalog and the rest are returned. 

158 """ 

159 self.config.doRemoveSkySources = True 

160 self.config.badFlagList = [] 

161 self.config.doRemoveNegativeDirectImageSources = False 

162 self.config.doWriteRejectedSkySources = True 

163 self.config.doTrailedSourceFilter = False 

164 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

165 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

166 nExpectedFilteredSources = self.nSources - self.nSkySources 

167 self.assertEqual(len(result.filteredDiaSourceCat), 

168 len(self.diaSourceCat[~self.diaSourceCat['sky_source']])) 

169 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFilteredSources) 

170 self.assertEqual(len(result.rejectedDiaSources), self.nSkySources) 

171 self.assertEqual(len(self.diaSourceCat), self.nSources) 

172 

173 def test_run_with_filter_defaultBadFlagList_only(self): 

174 """Test that when only the CR center filter is turned on the six sources which are flagged 

175 as base_PixelFlags_flag_crCenter are filtered out of the catalog and the rest are returned. 

176 """ 

177 self.config.doRemoveSkySources = False 

178 self.config.doRemoveNegativeDirectImageSources = False 

179 self.config.doWriteRejectedSkySources = False 

180 self.config.doTrailedSourceFilter = False 

181 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

182 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

183 nExpectedFilteredSources = self.nSources - self.nCrCenterSources 

184 self.assertEqual(len(result.filteredDiaSourceCat), 

185 len(self.diaSourceCat[~self.diaSourceCat['base_PixelFlags_flag_crCenter']])) 

186 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFilteredSources) 

187 self.assertEqual(len(result.rejectedDiaSources), self.nCrCenterSources) 

188 self.assertEqual(len(self.diaSourceCat), self.nSources) 

189 

190 def test_run_with_filter_nonDefaultBadFlagList_only(self): 

191 """Test that the badFlagList filters appropriately when it is not when the default configuration. 

192 The six sources flagged base_PixelFlags_flag_crCenter and the four sources flagged fakeBadFlag 

193 should be filtered out of the catalog and the rest are returned. 

194 """ 

195 self.config.doRemoveSkySources = False 

196 self.config.badFlagList = ["base_PixelFlags_flag_crCenter", "fakeBadFlag"] 

197 self.config.doRemoveNegativeDirectImageSources = False 

198 self.config.doWriteRejectedSkySources = False 

199 self.config.doTrailedSourceFilter = False 

200 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

201 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

202 nExpectedFilteredSources = self.nSources - self.nCrCenterSources - self.nFakeFlagSources 

203 self.assertEqual(len(result.filteredDiaSourceCat), 

204 len(self.diaSourceCat[~self.diaSourceCat['base_PixelFlags_flag_crCenter'] 

205 & ~self.diaSourceCat['fakeBadFlag']])) 

206 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFilteredSources) 

207 self.assertEqual(len(result.rejectedDiaSources), self.nCrCenterSources + self.nFakeFlagSources) 

208 self.assertEqual(len(self.diaSourceCat), self.nSources) 

209 

210 def test_run_with_filter_negative_only(self): 

211 """Test that when only the negative filter is turned on then 

212 sources which below the negtive snr cut are filtered out of the 

213 catalog and the rest are returned. 

214 """ 

215 self.config.doRemoveSkySources = False 

216 self.config.badFlagList = [] 

217 self.config.doRemoveNegativeDirectImageSources = True 

218 self.config.doWriteRejectedSkySources = True 

219 self.config.doTrailedSourceFilter = False 

220 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

221 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

222 nExpectedFilteredSources = self.nSources - self.nRemovedNegativeSources 

223 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFilteredSources) 

224 self.assertEqual(len(result.rejectedDiaSources), self.nRemovedNegativeSources) 

225 self.assertEqual(len(self.diaSourceCat), self.nSources) 

226 self.assertEqual(np.sum(result.filteredDiaSourceCat['ip_diffim_forced_PsfFlux_instFlux'] 

227 / result.filteredDiaSourceCat['ip_diffim_forced_PsfFlux_instFluxErr'] 

228 < self.config.minAllowedDirectSnr), 0) 

229 self.assertEqual(np.sum(result.rejectedDiaSources['ip_diffim_forced_PsfFlux_instFlux'] 

230 / result.rejectedDiaSources['ip_diffim_forced_PsfFlux_instFluxErr'] 

231 < self.config.minAllowedDirectSnr), 

232 self.nRemovedNegativeSources) 

233 

234 def test_run_with_filter_negative_and_sky(self): 

235 """Test concatenating rejects when both sky and negative filtering 

236 are on. 

237 """ 

238 self.config.doRemoveSkySources = True 

239 self.config.badFlagList = [] 

240 self.config.doRemoveNegativeDirectImageSources = True 

241 self.config.doWriteRejectedSkySources = True 

242 self.config.doTrailedSourceFilter = False 

243 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

244 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

245 nExpectedFilteredSources = self.nSources - self.nSkySources - self.nRemovedNegativeSources 

246 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFilteredSources) 

247 self.assertEqual(len(result.rejectedDiaSources), self.nSkySources + self.nRemovedNegativeSources) 

248 self.assertEqual(len(self.diaSourceCat), self.nSources) 

249 

250 def test_run_with_filter_reliability_only(self): 

251 """Test that when only the reliability filter is turned on, 

252 sources below the reliability threshold are filtered out.""" 

253 

254 reliability_threshold = 0.7 

255 config = FilterDiaSourceReliabilityConfig() 

256 config.minReliability = reliability_threshold 

257 

258 schema = afwTable.SourceTable.makeMinimalSchema() 

259 schema.addField("score", type="F", 

260 doc="Reliability of the source") 

261 reliabilityCat = afwTable.SourceCatalog(schema) 

262 reliabilityCat.reserve(self.nSources) 

263 

264 # Set reliability: first half below threshold, second half above 

265 for srcIdx in range(self.nSources): 

266 reliabilityCat.addNew() 

267 if srcIdx < self.nSources // 2: 

268 reliabilityCat[srcIdx]["score"] = 0.25 

269 else: 

270 reliabilityCat[srcIdx]["score"] = 0.95 

271 reliabilityCat['id'] = self.diaSourceCat['id'] 

272 nLowReliability = np.sum(reliabilityCat["score"] < 0.5) 

273 filterDiaSourceCatalogTask = FilterDiaSourceReliabilityTask(config=config) 

274 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, reliabilityCat) 

275 self.assertEqual(len(result.filteredDiaSources), self.nSources - nLowReliability) 

276 self.assertEqual(len(result.rejectedDiaSources), nLowReliability) 

277 self.assertTrue(np.all(result.filteredDiaSources["reliability"] >= reliability_threshold)) 

278 self.assertTrue(np.all(result.rejectedDiaSources["reliability"] < reliability_threshold)) 

279 

280 def test_run_with_filter_trailed_sources_only(self): 

281 """Test that when only the trail filter is turned on the correct number 

282 of sources are filtered out. The filtered sources should be the last 

283 three sources which have long trails, one source where both the suspect 

284 trail and edge trail flag are set, and one source where off_image is 

285 set. All sky objects should remain in the catalog. 

286 """ 

287 self.config.doRemoveSkySources = False 

288 self.config.badFlagList = [] 

289 self.config.doRemoveNegativeDirectImageSources = False 

290 self.config.doWriteRejectedSkySources = False 

291 self.config.doTrailedSourceFilter = True 

292 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

293 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

294 nExpectedFilteredSources = self.nSources - self.nFilteredTrailedSources 

295 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFilteredSources) 

296 self.assertEqual(len(self.diaSourceCat), self.nSources) 

297 

298 def test_run_with_all_filters(self): 

299 """Test that all sources are filtered out correctly. Only 15 sources 

300 should remain in the catalog after filtering. 

301 """ 

302 self.config.doRemoveSkySources = True 

303 self.config.doRemoveNegativeDirectImageSources = True 

304 self.config.doWriteRejectedSkySources = True 

305 self.config.doTrailedSourceFilter = True 

306 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

307 result = filterDiaSourceCatalogTask.run(self.diaSourceCat, self.visitInfo) 

308 nExpectedFilteredSources = (self.nSources - self.nSkySources 

309 - self.nCrCenterSources 

310 - self.nFilteredTrailedSources 

311 - self.nRemovedNegativeSources) 

312 nExpectedRejectedSources = (self.nSkySources 

313 + self.nCrCenterSources 

314 + self.nRemovedNegativeSources) 

315 # 32 total sources 

316 # 5 filtered out sky sources 

317 # 6 filtered out sources with cosmic ray detections 

318 # 2 filtered out negative sources 

319 # 4 filtered out trailed sources, 2 with long trails 2 with flags 

320 # 15 sources left 

321 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFilteredSources) 

322 # 17 sources rejected, 4 trailed sources not included, 13 rejected sources in catalog. 

323 self.assertEqual(len(result.rejectedDiaSources), nExpectedRejectedSources) 

324 self.assertEqual(len(self.diaSourceCat), self.nSources) 

325 

326 def test_pixelScale_calculation(self): 

327 """Check the calculation of the pixel scale from the input catalog. 

328 """ 

329 self.config.doTrailedSourceFilter = True 

330 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

331 scale = filterDiaSourceCatalogTask._estimate_pixel_scale(self.diaSourceCat) 

332 # Should be almost but not actually equal 

333 self.assertNotEqual(self.config.estimatedPixelScale, scale) 

334 self.assertAlmostEqual(self.config.estimatedPixelScale, scale, places=6) 

335 

336 # If the estimatedPixelScale is very different, that value should be 

337 # used exactly and it should not raise an error. 

338 self.config.estimatedPixelScale = 1.2 

339 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

340 scale = filterDiaSourceCatalogTask._estimate_pixel_scale(self.diaSourceCat) 

341 self.assertEqual(self.config.estimatedPixelScale, scale) 

342 

343 

344class MemoryTester(lsst.utils.tests.MemoryTestCase): 

345 pass 

346 

347 

348def setup_module(module): 

349 lsst.utils.tests.init() 

350 

351 

352if __name__ == "__main__": 352 ↛ 353line 352 didn't jump to line 353 because the condition on line 352 was never true

353 lsst.utils.tests.init() 

354 unittest.main()