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
« 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/>.
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 _, self.diaSourceCat = dataset.realize(10.0, schema, randomSeed=1234)
90 # set the sky_source flag for the first set
91 self.diaSourceCat[0:self.nSkySources]["sky_source"] = True
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
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
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
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
132 mjd = 57071.0
133 self.utc_jd = mjd + 2_400_000.5 - 35.0 / (24.0 * 60.0 * 60.0)
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 )
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)
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)
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)
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)
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)
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)
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."""
260 reliability_threshold = 0.7
261 config = FilterDiaSourceReliabilityConfig()
262 config.minReliability = reliability_threshold
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)
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))
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)
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)
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)
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)
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)
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
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)
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)
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
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"])
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)
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])
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
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)
447 bbox_mask = task._check_dia_source_trail_bbox(self.diaSourceCat, exposure_time)
448 self.assertTrue(bbox_mask[srcIdx])
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
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)
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])
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
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)
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)
506class MemoryTester(lsst.utils.tests.MemoryTestCase):
507 pass
510def setup_module(module):
511 lsst.utils.tests.init()
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()