Coverage for tests/test_filterDiaSourceCatalog.py: 99%
292 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 09:20 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-01 09:20 +0000
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/>.
22import unittest
23import numpy as np
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
39class TestFilterDiaSourceCatalogTask(unittest.TestCase):
41 def setUp(self):
42 self.config = FilterDiaSourceCatalogConfig()
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)
92 # set the sky_source flag for the first set
93 self.diaSourceCat[0:self.nSkySources]["sky_source"] = True
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
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
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
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
134 mjd = 57071.0
135 self.utc_jd = mjd + 2_400_000.5 - 35.0 / (24.0 * 60.0 * 60.0)
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 )
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)
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)
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)
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)
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)
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)
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."""
262 reliability_threshold = 0.7
263 config = FilterDiaSourceReliabilityConfig()
264 config.minReliability = reliability_threshold
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)
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))
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)
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)
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)
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)
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)
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
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)
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)
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
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"])
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)
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])
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
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)
451 bbox_mask = task._check_dia_source_trail_bbox(self.diaSourceCat, exposure_time)
452 self.assertTrue(bbox_mask[srcIdx])
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
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)
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])
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
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)
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)
510class MemoryTester(lsst.utils.tests.MemoryTestCase):
511 pass
514def setup_module(module):
515 lsst.utils.tests.init()
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()