Coverage for tests/test_filterDiaSourceCatalog.py: 99%

292 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-01 02:13 -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.afw.detection as afwDetect 

31import lsst.afw.geom as afwGeom 

32import lsst.meas.base.tests as measTests 

33import lsst.utils.tests 

34import lsst.afw.image as afwImage 

35import lsst.afw.table as afwTable 

36import lsst.daf.base as dafBase 

37 

38 

39class TestFilterDiaSourceCatalogTask(unittest.TestCase): 

40 

41 def setUp(self): 

42 self.config = FilterDiaSourceCatalogConfig() 

43 

44 self.nSkySources = 5 

45 self.nCrCenterSources = 6 

46 self.nFakeFlagSources = 4 

47 self.nNegativeSources = 7 

48 self.nTrailedSources = 10 

49 self.pixelScale = 0.2 # arcseconds/pixel 

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

51 + self.nNegativeSources + self.nTrailedSources) 

52 self.yLoc = 100 

53 self.expId = 4321 

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

55 geom.Extent2I(1024, 1153)) 

56 dataset = measTests.TestDataset(self.bbox) 

57 for srcIdx in range(self.nSources): 

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

59 schema = dataset.makeMinimalSchema() 

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

61 schema.addField("slot_Centroid_flag", type="Flag", doc="The centroid calculation " 

62 "failed. The source position is incorrect.") 

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

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

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

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

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

68 "than one item.") 

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

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

71 "centered at DiaSource position.") 

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

73 doc="Estimated uncertainty of ip_diffim_forced_PsfFlux_instFlux.") 

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

75 doc="General trail measurement failure flag") 

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

77 doc="Trail extends off image") 

78 schema.addField('ext_trailedSources_Naive_flag_suspect_long_trail', 

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

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

81 doc="Trail contains edge pixels") 

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

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

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

85 doc="Length of the source trail") 

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

87 doc="Reliability of the source") 

88 schema.addField("reliabilityVersion", type=str, size=7, 

89 doc="Version of the reliability model") 

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

91 

92 # set the sky_source flag for the first set 

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

94 

95 # set the pixelFlags_crCenter flag 

96 crCenter_offset = self.nSkySources 

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

98 "base_PixelFlags_flag_crCenter" 

99 ] = True 

100 

101 # set the fakeBadFlag flag 

102 fakeFlag_offset = crCenter_offset + self.nCrCenterSources 

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

104 "fakeBadFlag" 

105 ] = True 

106 

107 # create increasingly negative ip_diffim_forced_PsfFlux_instFlux/ip_diffim_forced_PsfFlux_instFluxErr 

108 self.nRemovedNegativeSources = 0 

109 negativeSources_offset = fakeFlag_offset + self.nFakeFlagSources 

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

111 negativeSources_offset+self.nNegativeSources)): 

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

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

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

115 self.nRemovedNegativeSources += 1 

116 

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

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

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

120 self.nFilteredTrailedSources = 0 

121 trail_offset = negativeSources_offset + self.nNegativeSources 

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

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

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

125 self.nFilteredTrailedSources += 1 

126 # Setting a combination of flags for filtering in tests 

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

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

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

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

131 # sources will be self.nFilteredTrailedSources + 2 

132 self.nFilteredTrailedSources += 2 

133 

134 mjd = 57071.0 

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

136 

137 self.visitInfo = afwImage.VisitInfo( 

138 # This incomplete visitInfo is sufficient for testing because the 

139 # Python constructor sets all other required values to some 

140 # default. 

141 exposureTime=30.0, 

142 darkTime=3.0, 

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

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

145 ) 

146 

147 def test_run_without_filter(self): 

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

149 are returned. 

150 """ 

151 self.config.doRemoveSkySources = False 

152 self.config.badFlagList = [] 

153 self.config.doRemoveNegativeDirectImageSources = False 

154 self.config.doWriteRejectedSkySources = False 

155 self.config.doTrailedSourceFilter = False 

156 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

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

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

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

161 

162 def test_run_with_filter_sky_only(self): 

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

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

165 catalog and the rest are returned. 

166 """ 

167 self.config.doRemoveSkySources = True 

168 self.config.badFlagList = [] 

169 self.config.doRemoveNegativeDirectImageSources = False 

170 self.config.doWriteRejectedSkySources = True 

171 self.config.doTrailedSourceFilter = False 

172 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

174 nExpectedFilteredSources = self.nSources - self.nSkySources 

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

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

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

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

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

180 

181 def test_run_with_filter_defaultBadFlagList_only(self): 

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

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

184 """ 

185 self.config.doRemoveSkySources = False 

186 self.config.doRemoveNegativeDirectImageSources = False 

187 self.config.doWriteRejectedSkySources = False 

188 self.config.doTrailedSourceFilter = False 

189 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

191 nExpectedFilteredSources = self.nSources - self.nCrCenterSources 

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

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

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

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

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

197 

198 def test_run_with_filter_nonDefaultBadFlagList_only(self): 

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

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

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

202 """ 

203 self.config.doRemoveSkySources = False 

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

205 self.config.doRemoveNegativeDirectImageSources = False 

206 self.config.doWriteRejectedSkySources = False 

207 self.config.doTrailedSourceFilter = False 

208 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

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

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

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

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

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

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

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

217 

218 def test_run_with_filter_negative_only(self): 

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

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

221 catalog and the rest are returned. 

222 """ 

223 self.config.doRemoveSkySources = False 

224 self.config.badFlagList = [] 

225 self.config.doRemoveNegativeDirectImageSources = True 

226 self.config.doWriteRejectedSkySources = True 

227 self.config.doTrailedSourceFilter = False 

228 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

230 nExpectedFilteredSources = self.nSources - self.nRemovedNegativeSources 

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

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

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

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

235 / result.filteredDiaSourceCat['ip_diffim_forced_PsfFlux_instFluxErr'] 

236 < self.config.minAllowedDirectSnr), 0) 

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

238 / result.rejectedDiaSources['ip_diffim_forced_PsfFlux_instFluxErr'] 

239 < self.config.minAllowedDirectSnr), 

240 self.nRemovedNegativeSources) 

241 

242 def test_run_with_filter_negative_and_sky(self): 

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

244 are on. 

245 """ 

246 self.config.doRemoveSkySources = True 

247 self.config.badFlagList = [] 

248 self.config.doRemoveNegativeDirectImageSources = True 

249 self.config.doWriteRejectedSkySources = True 

250 self.config.doTrailedSourceFilter = False 

251 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

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

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

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

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

257 

258 def test_run_with_filter_reliability_only(self): 

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

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

261 

262 reliability_threshold = 0.7 

263 config = FilterDiaSourceReliabilityConfig() 

264 config.minReliability = reliability_threshold 

265 

266 schema = afwTable.SourceTable.makeMinimalSchema() 

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

268 doc="Reliability of the source") 

269 schema.addField("version", type=str, size=7, 

270 doc="Version of the reliability model.") 

271 reliabilityCat = afwTable.SourceCatalog(schema) 

272 reliabilityCat.reserve(self.nSources) 

273 

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

275 for srcIdx in range(self.nSources): 

276 reliabilityCat.addNew() 

277 if srcIdx < self.nSources // 2: 

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

279 else: 

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

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

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

283 filterDiaSourceCatalogTask = FilterDiaSourceReliabilityTask(config=config) 

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

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

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

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

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

289 

290 def test_run_with_filter_trailed_sources_only(self): 

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

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

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

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

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

296 """ 

297 self.config.doRemoveSkySources = False 

298 self.config.badFlagList = [] 

299 self.config.doRemoveNegativeDirectImageSources = False 

300 self.config.doWriteRejectedSkySources = False 

301 self.config.doTrailedSourceFilter = True 

302 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

304 nExpectedFilteredSources = self.nSources - self.nFilteredTrailedSources 

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

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

307 

308 def test_run_with_all_filters(self): 

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

310 should remain in the catalog after filtering. 

311 """ 

312 self.config.doRemoveSkySources = True 

313 self.config.doRemoveNegativeDirectImageSources = True 

314 self.config.doWriteRejectedSkySources = True 

315 self.config.doTrailedSourceFilter = True 

316 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

318 nExpectedFilteredSources = (self.nSources - self.nSkySources 

319 - self.nCrCenterSources 

320 - self.nFilteredTrailedSources 

321 - self.nRemovedNegativeSources) 

322 nExpectedRejectedSources = (self.nSkySources 

323 + self.nCrCenterSources 

324 + self.nRemovedNegativeSources) 

325 # 32 total sources 

326 # 5 filtered out sky sources 

327 # 6 filtered out sources with cosmic ray detections 

328 # 2 filtered out negative sources 

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

330 # 15 sources left 

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

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

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

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

335 

336 def test_pixelScale_calculation(self): 

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

338 """ 

339 self.config.doTrailedSourceFilter = True 

340 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

341 scale = filterDiaSourceCatalogTask._estimate_pixel_scale(self.diaSourceCat) 

342 # Should be almost but not actually equal 

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

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

345 

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

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

348 self.config.estimatedPixelScale = 1.2 

349 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

350 scale = filterDiaSourceCatalogTask._estimate_pixel_scale(self.diaSourceCat) 

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

352 

353 def test_run_with_filter_centroid_flag(self): 

354 """Test that sources with slot_Centroid_flag set are filtered with the 

355 default badFlagList configuration. 

356 """ 

357 self.config.doRemoveSkySources = False 

358 self.config.doRemoveNegativeDirectImageSources = False 

359 self.config.doWriteRejectedSkySources = False 

360 self.config.doTrailedSourceFilter = False 

361 # Default badFlagList includes slot_Centroid_flag 

362 self.assertIn("slot_Centroid_flag", self.config.badFlagList) 

363 

364 # Set slot_Centroid_flag on a few sources that aren't already flagged 

365 nCentroidFlagged = 3 

366 # Pick sources at the end that have no other badFlagList flags 

367 trail_offset = (self.nSkySources + self.nCrCenterSources 

368 + self.nFakeFlagSources + self.nNegativeSources) 

369 for i in range(nCentroidFlagged): 

370 self.diaSourceCat[trail_offset + i]["slot_Centroid_flag"] = True 

371 

372 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

374 # Default badFlagList filters crCenter + slot_Centroid_flag + high_varianceCenterAll. 

375 # crCenter flags were already set for nCrCenterSources. 

376 # Our added centroid flags are on previously unflagged sources. 

377 nExpectedFiltered = self.nSources - self.nCrCenterSources - nCentroidFlagged 

378 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFiltered) 

379 

380 def test_check_dia_source_trail_bbox_no_flag(self): 

381 """Test that sources without ext_trailedSources_Naive_flag are not 

382 flagged by the bbox check, regardless of bbox size. 

383 """ 

384 self.config.doTrailedSourceFilter = True 

385 task = FilterDiaSourceCatalogTask(config=self.config) 

386 exposure_time = self.visitInfo.getExposureTime() 

387 # None of the sources have the trail flag set by default 

388 bbox_mask = task._check_dia_source_trail_bbox(self.diaSourceCat, exposure_time) 

389 self.assertEqual(np.sum(bbox_mask), 0) 

390 

391 def test_check_dia_source_trail_bbox_no_flag_large_bbox(self): 

392 """Test that sources without ext_trailedSources_Naive_flag are not 

393 flagged by the bbox check even when the bounding box is large enough 

394 that it would otherwise trigger the bbox filter. 

395 """ 

396 self.config.doTrailedSourceFilter = True 

397 task = FilterDiaSourceCatalogTask(config=self.config) 

398 exposure_time = self.visitInfo.getExposureTime() 

399 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

400 max_length_pixels = self.config.max_trail_length * exposure_time / pixelScale 

401 

402 # Give source 0 a large footprint exceeding the threshold, but do NOT 

403 # set ext_trailedSources_Naive_flag — the bbox check should be skipped. 

404 srcIdx = 0 

405 largeSpan = afwGeom.SpanSet( 

406 geom.Box2I(geom.Point2I(0, 0), 

407 geom.Extent2I(int(max_length_pixels) + 10, 5)) 

408 ) 

409 footprint = afwDetect.Footprint(largeSpan) 

410 self.diaSourceCat[srcIdx].setFootprint(footprint) 

411 self.assertFalse(self.diaSourceCat[srcIdx]["ext_trailedSources_Naive_flag"]) 

412 

413 bbox_mask = task._check_dia_source_trail_bbox(self.diaSourceCat, exposure_time) 

414 self.assertFalse(bbox_mask[srcIdx]) 

415 self.assertEqual(np.sum(bbox_mask), 0) 

416 

417 def test_check_dia_source_trail_bbox_flag_small_bbox(self): 

418 """Test that sources with ext_trailedSources_Naive_flag set but a small bounding 

419 box are not flagged. 

420 """ 

421 self.config.doTrailedSourceFilter = True 

422 task = FilterDiaSourceCatalogTask(config=self.config) 

423 exposure_time = self.visitInfo.getExposureTime() 

424 # Set the trail flag on a source with a small footprint (PSF-sized) 

425 self.diaSourceCat[0]["ext_trailedSources_Naive_flag"] = True 

426 bbox_mask = task._check_dia_source_trail_bbox(self.diaSourceCat, exposure_time) 

427 # The default PSF footprint is small (~7 pixels), well below the 

428 # threshold for max_trail_length * exposure_time / pixelScale 

429 self.assertFalse(bbox_mask[0]) 

430 

431 def test_check_dia_source_trail_bbox_flag_large_bbox(self): 

432 """Test that sources with ext_trailedSources_Naive_flag set and a 

433 large bounding box are flagged. 

434 """ 

435 self.config.doTrailedSourceFilter = True 

436 task = FilterDiaSourceCatalogTask(config=self.config) 

437 exposure_time = self.visitInfo.getExposureTime() 

438 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

439 max_length_pixels = self.config.max_trail_length * exposure_time / pixelScale 

440 

441 # Set the trail flag on a source and give it a large footprint 

442 srcIdx = 0 

443 self.diaSourceCat[srcIdx]["ext_trailedSources_Naive_flag"] = True 

444 largeSpan = afwGeom.SpanSet( 

445 geom.Box2I(geom.Point2I(0, 0), 

446 geom.Extent2I(int(max_length_pixels) + 10, 5)) 

447 ) 

448 footprint = afwDetect.Footprint(largeSpan) 

449 self.diaSourceCat[srcIdx].setFootprint(footprint) 

450 

451 bbox_mask = task._check_dia_source_trail_bbox(self.diaSourceCat, exposure_time) 

452 self.assertTrue(bbox_mask[srcIdx]) 

453 

454 def test_check_dia_source_trail_bbox_diagonal(self): 

455 """Test a source with a large diagonal but small width/height should still 

456 be flagged. 

457 """ 

458 self.config.doTrailedSourceFilter = True 

459 task = FilterDiaSourceCatalogTask(config=self.config) 

460 exposure_time = self.visitInfo.getExposureTime() 

461 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

462 max_length_pixels = self.config.max_trail_length * exposure_time / pixelScale 

463 

464 srcIdx = 1 

465 self.diaSourceCat[srcIdx]["ext_trailedSources_Naive_flag"] = True 

466 # Create a bbox whose individual sides are below max_length_pixels 

467 # but whose diagonal exceeds it. 

468 side = int(max_length_pixels * 0.8) 

469 largeSpan = afwGeom.SpanSet( 

470 geom.Box2I(geom.Point2I(0, 0), geom.Extent2I(side, side)) 

471 ) 

472 footprint = afwDetect.Footprint(largeSpan) 

473 self.diaSourceCat[srcIdx].setFootprint(footprint) 

474 

475 bbox_mask = task._check_dia_source_trail_bbox(self.diaSourceCat, exposure_time) 

476 # diagonal = side * sqrt(2) ~ max_length_pixels * 1.13 > max_length_pixels 

477 self.assertTrue(bbox_mask[srcIdx]) 

478 

479 def test_run_with_trail_and_bbox_filter(self): 

480 """Test doTrailedSourceFilter=True filters sources 

481 via both trail length and bbox fallback. 

482 """ 

483 self.config.doRemoveSkySources = False 

484 self.config.badFlagList = [] 

485 self.config.doRemoveNegativeDirectImageSources = False 

486 self.config.doWriteRejectedSkySources = False 

487 self.config.doTrailedSourceFilter = True 

488 task = FilterDiaSourceCatalogTask(config=self.config) 

489 exposure_time = self.visitInfo.getExposureTime() 

490 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

491 max_length_pixels = self.config.max_trail_length * exposure_time / pixelScale 

492 

493 # Set the trail flag and give a large footprint to a source that 

494 # wouldn't be caught by the trail length check (first source). 

495 srcIdx = 0 

496 self.diaSourceCat[srcIdx]["ext_trailedSources_Naive_flag"] = True 

497 largeSpan = afwGeom.SpanSet( 

498 geom.Box2I(geom.Point2I(0, 0), 

499 geom.Extent2I(int(max_length_pixels) + 10, 5)) 

500 ) 

501 footprint = afwDetect.Footprint(largeSpan) 

502 self.diaSourceCat[srcIdx].setFootprint(footprint) 

503 

504 result = task.run(self.diaSourceCat, self.visitInfo) 

505 # The bbox-flagged source should have been removed 

506 nExpectedFiltered = self.nSources - self.nFilteredTrailedSources - 1 

507 self.assertEqual(len(result.filteredDiaSourceCat), nExpectedFiltered) 

508 

509 

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

511 pass 

512 

513 

514def setup_module(module): 

515 lsst.utils.tests.init() 

516 

517 

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

519 lsst.utils.tests.init() 

520 unittest.main()