Coverage for tests/test_filterDiaSourceCatalog.py: 99%

290 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-26 00:50 -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 _, self.diaSourceCat = dataset.realize(10.0, schema, randomSeed=1234) 

89 

90 # set the sky_source flag for the first set 

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

92 

93 # set the pixelFlags_crCenter flag 

94 crCenter_offset = self.nSkySources 

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

96 "base_PixelFlags_flag_crCenter" 

97 ] = True 

98 

99 # set the fakeBadFlag flag 

100 fakeFlag_offset = crCenter_offset + self.nCrCenterSources 

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

102 "fakeBadFlag" 

103 ] = True 

104 

105 # create increasingly negative ip_diffim_forced_PsfFlux_instFlux/ip_diffim_forced_PsfFlux_instFluxErr 

106 self.nRemovedNegativeSources = 0 

107 negativeSources_offset = fakeFlag_offset + self.nFakeFlagSources 

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

109 negativeSources_offset+self.nNegativeSources)): 

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

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

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

113 self.nRemovedNegativeSources += 1 

114 

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

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

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

118 self.nFilteredTrailedSources = 0 

119 trail_offset = negativeSources_offset + self.nNegativeSources 

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

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

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

123 self.nFilteredTrailedSources += 1 

124 # Setting a combination of flags for filtering in tests 

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

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

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

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

129 # sources will be self.nFilteredTrailedSources + 2 

130 self.nFilteredTrailedSources += 2 

131 

132 mjd = 57071.0 

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

134 

135 self.visitInfo = afwImage.VisitInfo( 

136 # This incomplete visitInfo is sufficient for testing because the 

137 # Python constructor sets all other required values to some 

138 # default. 

139 exposureTime=30.0, 

140 darkTime=3.0, 

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

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

143 ) 

144 

145 def test_run_without_filter(self): 

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

147 are returned. 

148 """ 

149 self.config.doRemoveSkySources = False 

150 self.config.badFlagList = [] 

151 self.config.doRemoveNegativeDirectImageSources = False 

152 self.config.doWriteRejectedSkySources = False 

153 self.config.doTrailedSourceFilter = False 

154 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

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

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

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

159 

160 def test_run_with_filter_sky_only(self): 

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

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

163 catalog and the rest are returned. 

164 """ 

165 self.config.doRemoveSkySources = True 

166 self.config.badFlagList = [] 

167 self.config.doRemoveNegativeDirectImageSources = False 

168 self.config.doWriteRejectedSkySources = True 

169 self.config.doTrailedSourceFilter = False 

170 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

172 nExpectedFilteredSources = self.nSources - self.nSkySources 

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

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

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

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

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

178 

179 def test_run_with_filter_defaultBadFlagList_only(self): 

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

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

182 """ 

183 self.config.doRemoveSkySources = False 

184 self.config.doRemoveNegativeDirectImageSources = False 

185 self.config.doWriteRejectedSkySources = False 

186 self.config.doTrailedSourceFilter = False 

187 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

189 nExpectedFilteredSources = self.nSources - self.nCrCenterSources 

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

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

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

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

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

195 

196 def test_run_with_filter_nonDefaultBadFlagList_only(self): 

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

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

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

200 """ 

201 self.config.doRemoveSkySources = False 

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

203 self.config.doRemoveNegativeDirectImageSources = False 

204 self.config.doWriteRejectedSkySources = False 

205 self.config.doTrailedSourceFilter = False 

206 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

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

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

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

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

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

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

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

215 

216 def test_run_with_filter_negative_only(self): 

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

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

219 catalog and the rest are returned. 

220 """ 

221 self.config.doRemoveSkySources = False 

222 self.config.badFlagList = [] 

223 self.config.doRemoveNegativeDirectImageSources = True 

224 self.config.doWriteRejectedSkySources = True 

225 self.config.doTrailedSourceFilter = False 

226 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

228 nExpectedFilteredSources = self.nSources - self.nRemovedNegativeSources 

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

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

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

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

233 / result.filteredDiaSourceCat['ip_diffim_forced_PsfFlux_instFluxErr'] 

234 < self.config.minAllowedDirectSnr), 0) 

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

236 / result.rejectedDiaSources['ip_diffim_forced_PsfFlux_instFluxErr'] 

237 < self.config.minAllowedDirectSnr), 

238 self.nRemovedNegativeSources) 

239 

240 def test_run_with_filter_negative_and_sky(self): 

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

242 are on. 

243 """ 

244 self.config.doRemoveSkySources = True 

245 self.config.badFlagList = [] 

246 self.config.doRemoveNegativeDirectImageSources = True 

247 self.config.doWriteRejectedSkySources = True 

248 self.config.doTrailedSourceFilter = False 

249 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

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

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

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

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

255 

256 def test_run_with_filter_reliability_only(self): 

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

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

259 

260 reliability_threshold = 0.7 

261 config = FilterDiaSourceReliabilityConfig() 

262 config.minReliability = reliability_threshold 

263 

264 schema = afwTable.SourceTable.makeMinimalSchema() 

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

266 doc="Reliability of the source") 

267 reliabilityCat = afwTable.SourceCatalog(schema) 

268 reliabilityCat.reserve(self.nSources) 

269 

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

271 for srcIdx in range(self.nSources): 

272 reliabilityCat.addNew() 

273 if srcIdx < self.nSources // 2: 

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

275 else: 

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

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

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

279 filterDiaSourceCatalogTask = FilterDiaSourceReliabilityTask(config=config) 

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

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

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

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

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

285 

286 def test_run_with_filter_trailed_sources_only(self): 

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

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

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

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

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

292 """ 

293 self.config.doRemoveSkySources = False 

294 self.config.badFlagList = [] 

295 self.config.doRemoveNegativeDirectImageSources = False 

296 self.config.doWriteRejectedSkySources = False 

297 self.config.doTrailedSourceFilter = True 

298 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

300 nExpectedFilteredSources = self.nSources - self.nFilteredTrailedSources 

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

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

303 

304 def test_run_with_all_filters(self): 

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

306 should remain in the catalog after filtering. 

307 """ 

308 self.config.doRemoveSkySources = True 

309 self.config.doRemoveNegativeDirectImageSources = True 

310 self.config.doWriteRejectedSkySources = True 

311 self.config.doTrailedSourceFilter = True 

312 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

314 nExpectedFilteredSources = (self.nSources - self.nSkySources 

315 - self.nCrCenterSources 

316 - self.nFilteredTrailedSources 

317 - self.nRemovedNegativeSources) 

318 nExpectedRejectedSources = (self.nSkySources 

319 + self.nCrCenterSources 

320 + self.nRemovedNegativeSources) 

321 # 32 total sources 

322 # 5 filtered out sky sources 

323 # 6 filtered out sources with cosmic ray detections 

324 # 2 filtered out negative sources 

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

326 # 15 sources left 

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

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

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

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

331 

332 def test_pixelScale_calculation(self): 

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

334 """ 

335 self.config.doTrailedSourceFilter = True 

336 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

337 scale = filterDiaSourceCatalogTask._estimate_pixel_scale(self.diaSourceCat) 

338 # Should be almost but not actually equal 

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

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

341 

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

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

344 self.config.estimatedPixelScale = 1.2 

345 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

346 scale = filterDiaSourceCatalogTask._estimate_pixel_scale(self.diaSourceCat) 

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

348 

349 def test_run_with_filter_centroid_flag(self): 

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

351 default badFlagList configuration. 

352 """ 

353 self.config.doRemoveSkySources = False 

354 self.config.doRemoveNegativeDirectImageSources = False 

355 self.config.doWriteRejectedSkySources = False 

356 self.config.doTrailedSourceFilter = False 

357 # Default badFlagList includes slot_Centroid_flag 

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

359 

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

361 nCentroidFlagged = 3 

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

363 trail_offset = (self.nSkySources + self.nCrCenterSources 

364 + self.nFakeFlagSources + self.nNegativeSources) 

365 for i in range(nCentroidFlagged): 

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

367 

368 filterDiaSourceCatalogTask = FilterDiaSourceCatalogTask(config=self.config) 

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

370 # Default badFlagList filters crCenter + slot_Centroid_flag + high_varianceCenterAll. 

371 # crCenter flags were already set for nCrCenterSources. 

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

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

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

375 

376 def test_check_dia_source_trail_bbox_no_flag(self): 

377 """Test that sources without ext_trailedSources_Naive_flag are not 

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

379 """ 

380 self.config.doTrailedSourceFilter = True 

381 task = FilterDiaSourceCatalogTask(config=self.config) 

382 exposure_time = self.visitInfo.getExposureTime() 

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

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

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

386 

387 def test_check_dia_source_trail_bbox_no_flag_large_bbox(self): 

388 """Test that sources without ext_trailedSources_Naive_flag are not 

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

390 that it would otherwise trigger the bbox filter. 

391 """ 

392 self.config.doTrailedSourceFilter = True 

393 task = FilterDiaSourceCatalogTask(config=self.config) 

394 exposure_time = self.visitInfo.getExposureTime() 

395 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

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

397 

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

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

400 srcIdx = 0 

401 largeSpan = afwGeom.SpanSet( 

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

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

404 ) 

405 footprint = afwDetect.Footprint(largeSpan) 

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

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

408 

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

410 self.assertFalse(bbox_mask[srcIdx]) 

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

412 

413 def test_check_dia_source_trail_bbox_flag_small_bbox(self): 

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

415 box are not flagged. 

416 """ 

417 self.config.doTrailedSourceFilter = True 

418 task = FilterDiaSourceCatalogTask(config=self.config) 

419 exposure_time = self.visitInfo.getExposureTime() 

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

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

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

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

424 # threshold for max_trail_length * exposure_time / pixelScale 

425 self.assertFalse(bbox_mask[0]) 

426 

427 def test_check_dia_source_trail_bbox_flag_large_bbox(self): 

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

429 large bounding box are flagged. 

430 """ 

431 self.config.doTrailedSourceFilter = True 

432 task = FilterDiaSourceCatalogTask(config=self.config) 

433 exposure_time = self.visitInfo.getExposureTime() 

434 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

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

436 

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

438 srcIdx = 0 

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

440 largeSpan = afwGeom.SpanSet( 

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

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

443 ) 

444 footprint = afwDetect.Footprint(largeSpan) 

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

446 

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

448 self.assertTrue(bbox_mask[srcIdx]) 

449 

450 def test_check_dia_source_trail_bbox_diagonal(self): 

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

452 be flagged. 

453 """ 

454 self.config.doTrailedSourceFilter = True 

455 task = FilterDiaSourceCatalogTask(config=self.config) 

456 exposure_time = self.visitInfo.getExposureTime() 

457 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

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

459 

460 srcIdx = 1 

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

462 # Create a bbox whose individual sides are below max_length_pixels 

463 # but whose diagonal exceeds it. 

464 side = int(max_length_pixels * 0.8) 

465 largeSpan = afwGeom.SpanSet( 

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

467 ) 

468 footprint = afwDetect.Footprint(largeSpan) 

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

470 

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

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

473 self.assertTrue(bbox_mask[srcIdx]) 

474 

475 def test_run_with_trail_and_bbox_filter(self): 

476 """Test doTrailedSourceFilter=True filters sources 

477 via both trail length and bbox fallback. 

478 """ 

479 self.config.doRemoveSkySources = False 

480 self.config.badFlagList = [] 

481 self.config.doRemoveNegativeDirectImageSources = False 

482 self.config.doWriteRejectedSkySources = False 

483 self.config.doTrailedSourceFilter = True 

484 task = FilterDiaSourceCatalogTask(config=self.config) 

485 exposure_time = self.visitInfo.getExposureTime() 

486 pixelScale = task._estimate_pixel_scale(self.diaSourceCat) 

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

488 

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

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

491 srcIdx = 0 

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

493 largeSpan = afwGeom.SpanSet( 

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

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

496 ) 

497 footprint = afwDetect.Footprint(largeSpan) 

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

499 

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

501 # The bbox-flagged source should have been removed 

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

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

504 

505 

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

507 pass 

508 

509 

510def setup_module(module): 

511 lsst.utils.tests.init() 

512 

513 

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

515 lsst.utils.tests.init() 

516 unittest.main()