Coverage for tests/test_sourceSelector.py: 14%
280 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-31 10:58 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-01-31 10:58 +0000
1#
2# LSST Data Management System
3#
4# Copyright 2008-2017 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
24import unittest
25import numpy as np
26import astropy.units as u
28import lsst.afw.table
29import lsst.meas.algorithms
30import lsst.meas.base.tests
31import lsst.pipe.base
32import lsst.utils.tests
34from lsst.meas.algorithms import ColorLimit
37class SourceSelectorTester:
38 """Mixin for testing
40 This provides a base class for doing tests common to the
41 ScienceSourceSelectorTask and ReferenceSourceSelectorTask.
42 """
43 Task = None
45 def setUp(self):
46 schema = lsst.afw.table.SourceTable.makeMinimalSchema()
47 schema.addField("flux", float, "Flux value")
48 schema.addField("flux_flag", "Flag", "Bad flux?")
49 schema.addField("other_flux", float, "Flux value 2")
50 schema.addField("other_flux_flag", "Flag", "Bad flux 2?")
51 schema.addField("other_fluxErr", float, "Flux error 2")
52 schema.addField("goodFlag", "Flag", "Flagged if good")
53 schema.addField("badFlag", "Flag", "Flagged if bad")
54 schema.addField("starGalaxy", float, "0=star, 1=galaxy")
55 schema.addField("nChild", np.int32, "Number of children")
56 schema.addField("detect_isPrimary", "Flag", "Is primary detection?")
57 self.catalog = lsst.afw.table.SourceCatalog(schema)
58 self.catalog.reserve(10)
59 self.config = self.Task.ConfigClass()
61 def tearDown(self):
62 del self.catalog
64 def check(self, expected):
65 task = self.Task(config=self.config)
66 results = task.run(self.catalog)
67 self.assertListEqual(results.selected.tolist(), expected)
68 self.assertListEqual([src.getId() for src in results.sourceCat],
69 [src.getId() for src, ok in zip(self.catalog, expected) if ok])
71 # Check with pandas.DataFrame version of catalog
72 results = task.run(self.catalog.asAstropy().to_pandas())
73 self.assertListEqual(results.selected.tolist(), expected)
74 self.assertListEqual(list(results.sourceCat['id']),
75 [src.getId() for src, ok in zip(self.catalog, expected) if ok])
77 # Check with astropy.table.Table version of catalog
78 results = task.run(self.catalog.asAstropy())
79 self.assertListEqual(results.selected.tolist(), expected)
80 self.assertListEqual(list(results.sourceCat['id']),
81 [src.getId() for src, ok in zip(self.catalog, expected) if ok])
83 def testFlags(self):
84 bad1 = self.catalog.addNew()
85 bad1.set("goodFlag", False)
86 bad1.set("badFlag", False)
87 bad2 = self.catalog.addNew()
88 bad2.set("goodFlag", True)
89 bad2.set("badFlag", True)
90 bad3 = self.catalog.addNew()
91 bad3.set("goodFlag", False)
92 bad3.set("badFlag", True)
93 good = self.catalog.addNew()
94 good.set("goodFlag", True)
95 good.set("badFlag", False)
96 self.catalog["flux"] = 1.0
97 self.catalog["other_flux"] = 1.0
98 self.config.flags.good = ["goodFlag"]
99 self.config.flags.bad = ["badFlag"]
100 self.check([False, False, False, True])
102 def testSignalToNoise(self):
103 low = self.catalog.addNew()
104 low.set("other_flux", 1.0)
105 low.set("other_fluxErr", 1.0)
106 good = self.catalog.addNew()
107 good.set("other_flux", 1.0)
108 good.set("other_fluxErr", 0.1)
109 high = self.catalog.addNew()
110 high.set("other_flux", 1.0)
111 high.set("other_fluxErr", 0.001)
112 self.config.doSignalToNoise = True
113 self.config.signalToNoise.fluxField = "other_flux"
114 self.config.signalToNoise.errField = "other_fluxErr"
115 self.config.signalToNoise.minimum = 5.0
116 self.config.signalToNoise.maximum = 100.0
117 self.check([False, True, False])
120class ScienceSourceSelectorTaskTest(SourceSelectorTester, lsst.utils.tests.TestCase):
121 Task = lsst.meas.algorithms.ScienceSourceSelectorTask
123 def setUp(self):
124 SourceSelectorTester.setUp(self)
125 self.config.fluxLimit.fluxField = "flux"
126 self.config.flags.bad = []
127 self.config.doFluxLimit = True
128 self.config.doFlags = True
129 self.config.doUnresolved = False
130 self.config.doIsolated = False
132 def testFluxLimit(self):
133 tooBright = self.catalog.addNew()
134 tooBright.set("flux", 1.0e10)
135 tooBright.set("flux_flag", False)
136 good = self.catalog.addNew()
137 good.set("flux", 1000.0)
138 good.set("flux_flag", False)
139 bad = self.catalog.addNew()
140 bad.set("flux", good.get("flux"))
141 bad.set("flux_flag", True)
142 tooFaint = self.catalog.addNew()
143 tooFaint.set("flux", 1.0)
144 tooFaint.set("flux_flag", False)
145 self.config.fluxLimit.minimum = 10.0
146 self.config.fluxLimit.maximum = 1.0e6
147 self.config.fluxLimit.fluxField = "flux"
148 self.check([False, True, False, False])
150 # Works with no maximum set?
151 self.config.fluxLimit.maximum = None
152 self.check([True, True, False, False])
154 # Works with no minimum set?
155 self.config.fluxLimit.minimum = None
156 self.check([True, True, False, True])
158 def testUnresolved(self):
159 num = 5
160 for _ in range(num):
161 self.catalog.addNew()
162 self.catalog["flux"] = 1.0
163 starGalaxy = np.linspace(0.0, 1.0, num, False)
164 self.catalog["starGalaxy"] = starGalaxy
165 self.config.doUnresolved = True
166 self.config.unresolved.name = "starGalaxy"
167 minimum, maximum = 0.3, 0.7
168 self.config.unresolved.minimum = minimum
169 self.config.unresolved.maximum = maximum
170 self.check(((starGalaxy > minimum) & (starGalaxy < maximum)).tolist())
172 # Works with no minimum set?
173 self.config.unresolved.minimum = None
174 self.check((starGalaxy < maximum).tolist())
176 # Works with no maximum set?
177 self.config.unresolved.minimum = minimum
178 self.config.unresolved.maximum = None
179 self.check((starGalaxy > minimum).tolist())
181 def testIsolated(self):
182 num = 5
183 for _ in range(num):
184 self.catalog.addNew()
185 self.catalog["flux"] = 1.0
186 parent = np.array([0, 0, 10, 0, 0], dtype=int)
187 nChild = np.array([2, 0, 0, 0, 0], dtype=int)
188 self.catalog["parent"] = parent
189 self.catalog["nChild"] = nChild
190 self.config.doIsolated = True
191 self.config.isolated.parentName = "parent"
192 self.config.isolated.nChildName = "nChild"
193 self.check(((parent == 0) & (nChild == 0)).tolist())
195 def testRequireFiniteRaDec(self):
196 num = 5
197 for _ in range(num):
198 self.catalog.addNew()
199 ra = np.array([np.nan, np.nan, 0, 0, 0], dtype=float)
200 dec = np.array([2, np.nan, 0, 0, np.nan], dtype=float)
201 self.catalog["coord_ra"] = ra
202 self.catalog["coord_dec"] = dec
203 self.config.doRequireFiniteRaDec = True
204 self.config.requireFiniteRaDec.raColName = "coord_ra"
205 self.config.requireFiniteRaDec.decColName = "coord_dec"
206 self.check((np.isfinite(ra) & np.isfinite(dec)).tolist())
208 def testRequirePrimary(self):
209 num = 5
210 for _ in range(num):
211 self.catalog.addNew()
212 primary = np.array([True, True, False, True, False], dtype=bool)
213 self.catalog["detect_isPrimary"] = primary
214 self.config.doRequirePrimary = True
215 self.config.requirePrimary.primaryColName = "detect_isPrimary"
216 self.check(primary.tolist())
219class ReferenceSourceSelectorTaskTest(SourceSelectorTester, lsst.utils.tests.TestCase):
220 Task = lsst.meas.algorithms.ReferenceSourceSelectorTask
222 def setUp(self):
223 SourceSelectorTester.setUp(self)
224 self.config.magLimit.fluxField = "flux"
225 self.config.doMagLimit = True
226 self.config.doFlags = True
227 self.config.doUnresolved = False
228 self.config.doRequireFiniteRaDec = False
230 def testMagnitudeLimit(self):
231 tooBright = self.catalog.addNew()
232 tooBright.set("flux", 1.0e10)
233 tooBright.set("flux_flag", False)
234 good = self.catalog.addNew()
235 good.set("flux", 1000.0)
236 good.set("flux_flag", False)
237 bad = self.catalog.addNew()
238 bad.set("flux", good.get("flux"))
239 bad.set("flux_flag", True)
240 tooFaint = self.catalog.addNew()
241 tooFaint.set("flux", 1.0)
242 tooFaint.set("flux_flag", False)
243 # Note: magnitudes are backwards, so the minimum flux is the maximum magnitude
244 self.config.magLimit.minimum = (1.0e6*u.nJy).to_value(u.ABmag)
245 self.config.magLimit.maximum = (10.0*u.nJy).to_value(u.ABmag)
246 self.config.magLimit.fluxField = "flux"
247 self.check([False, True, False, False])
249 # Works with no minimum set?
250 self.config.magLimit.minimum = None
251 self.check([True, True, False, False])
253 # Works with no maximum set?
254 self.config.magLimit.maximum = None
255 self.check([True, True, False, True])
257 def testMagErrorLimit(self):
258 # Using an arbitrary field as if it was a magnitude error to save adding a new field
259 field = "other_fluxErr"
260 tooFaint = self.catalog.addNew()
261 tooFaint.set(field, 0.5)
262 tooBright = self.catalog.addNew()
263 tooBright.set(field, 0.00001)
264 good = self.catalog.addNew()
265 good.set(field, 0.2)
267 self.config.doMagError = True
268 self.config.magError.minimum = 0.01
269 self.config.magError.maximum = 0.3
270 self.config.magError.magErrField = field
271 self.check([False, False, True])
273 def testColorLimits(self):
274 num = 10
275 for _ in range(num):
276 self.catalog.addNew()
277 color = np.linspace(-0.5, 0.5, num, True)
278 flux = 1000.0*u.nJy
279 # Definition: color = mag(flux) - mag(otherFlux)
280 otherFlux = (flux.to(u.ABmag) - color*u.mag).to_value(u.nJy)
281 self.catalog["flux"] = flux.value
282 self.catalog["other_flux"] = otherFlux
283 minimum, maximum = -0.1, 0.2
284 self.config.colorLimits = {"test": ColorLimit(primary="flux", secondary="other_flux",
285 minimum=minimum, maximum=maximum)}
286 self.check(((color > minimum) & (color < maximum)).tolist())
288 # Works with no minimum set?
289 self.config.colorLimits["test"].minimum = None
290 self.check((color < maximum).tolist())
292 # Works with no maximum set?
293 self.config.colorLimits["test"].maximum = None
294 self.config.colorLimits["test"].minimum = minimum
295 self.check((color > minimum).tolist())
297 # Multiple limits
298 self.config.colorLimits = {"test": ColorLimit(primary="flux", secondary="other_flux",
299 minimum=minimum),
300 "other": ColorLimit(primary="flux", secondary="other_flux",
301 maximum=maximum)}
302 assert maximum > minimum # To be non-mutually-exclusive
303 self.check(((color > minimum) & (color < maximum)).tolist())
305 # Multiple mutually-exclusive limits
306 self.config.colorLimits["test"] = ColorLimit(primary="flux", secondary="other_flux", maximum=-0.1)
307 self.config.colorLimits["other"] = ColorLimit(primary="flux", secondary="other_flux", minimum=0.1)
308 self.check([False]*num)
310 def testUnresolved(self):
311 num = 5
312 for _ in range(num):
313 self.catalog.addNew()
314 self.catalog["flux"] = 1.0
315 starGalaxy = np.linspace(0.0, 1.0, num, False)
316 self.catalog["starGalaxy"] = starGalaxy
317 self.config.doUnresolved = True
318 self.config.unresolved.name = "starGalaxy"
319 minimum, maximum = 0.3, 0.7
320 self.config.unresolved.minimum = minimum
321 self.config.unresolved.maximum = maximum
322 self.check(((starGalaxy > minimum) & (starGalaxy < maximum)).tolist())
324 # Works with no minimum set?
325 self.config.unresolved.minimum = None
326 self.check((starGalaxy < maximum).tolist())
328 # Works with no maximum set?
329 self.config.unresolved.minimum = minimum
330 self.config.unresolved.maximum = None
331 self.check((starGalaxy > minimum).tolist())
333 def testFiniteRaDec(self):
334 "Test that non-finite RA and Dec values are caught."
335 num = 5
336 for _ in range(num):
337 self.catalog.addNew()
338 self.catalog["coord_ra"][:] = 1.0
339 self.catalog["coord_dec"][:] = 1.0
340 self.catalog["coord_ra"][0] = np.nan
341 self.catalog["coord_dec"][1] = np.inf
342 self.config.doRequireFiniteRaDec = True
344 self.check([False, False, True, True, True])
347class TestBaseSourceSelector(lsst.utils.tests.TestCase):
348 """Test the API of the Abstract Base Class with a trivial example."""
349 def setUp(self):
350 schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema()
351 self.selectedKeyName = "is_selected"
352 schema.addField(self.selectedKeyName, type="Flag")
353 self.catalog = lsst.afw.table.SourceCatalog(schema)
354 for i in range(4):
355 self.catalog.addNew()
357 self.sourceSelector = lsst.meas.algorithms.NullSourceSelectorTask()
359 def testRun(self):
360 """Test that run() returns a catalog and boolean selected array."""
361 result = self.sourceSelector.run(self.catalog)
362 for i, x in enumerate(self.catalog['id']):
363 self.assertIn(x, result.sourceCat['id'])
364 self.assertTrue(result.selected[i])
366 def testRunSourceSelectedField(self):
367 """Test that the selected flag is set in the original catalog."""
368 self.sourceSelector.run(self.catalog, sourceSelectedField=self.selectedKeyName)
369 np.testing.assert_array_equal(self.catalog[self.selectedKeyName], True)
371 def testRunNonContiguousRaises(self):
372 """Cannot do source selection on non-contiguous catalogs."""
373 del self.catalog[1] # take one out of the middle to make it non-contiguous.
374 self.assertFalse(self.catalog.isContiguous(), "Catalog is contiguous: the test won't work.")
376 with self.assertRaises(RuntimeError):
377 self.sourceSelector.run(self.catalog)
380class TestMemory(lsst.utils.tests.MemoryTestCase):
381 pass
384def setup_module(module):
385 lsst.utils.tests.init()
388if __name__ == "__main__": 388 ↛ 389line 388 didn't jump to line 389, because the condition on line 388 was never true
389 import sys
390 setup_module(sys.modules[__name__])
391 unittest.main()