Coverage for tests/test_measureApCorr.py: 16%
172 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-23 02:20 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-23 02:20 -0700
1#
2# LSST Data Management System
3#
4# Copyright 2008-2016 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#
23import unittest
24import numpy as np
26import lsst.geom
27import lsst.afw.image as afwImage
28import lsst.afw.table as afwTable
29from lsst.afw.math import ChebyshevBoundedField
30import lsst.pex.config
31import lsst.meas.algorithms.measureApCorr as measureApCorr
32from lsst.meas.base.apCorrRegistry import addApCorrName
33import lsst.meas.base.tests
34import lsst.utils.tests
37def apCorrDefaultMap(value=None, bbox=None):
38 default_coefficients = np.ones((1, 1), dtype=float)
39 default_coefficients /= value
40 default_apCorrMap = ChebyshevBoundedField(bbox, default_coefficients)
41 default_fill = afwImage.ImageF(bbox)
42 default_apCorrMap.fillImage(default_fill)
43 return(default_fill)
46class MeasureApCorrTestCase(lsst.meas.base.tests.AlgorithmTestCase, lsst.utils.tests.TestCase):
48 def makeCatalog(self, apCorrScale=1.0, numSources=5):
49 sourceCat = afwTable.SourceCatalog(self.schema)
51 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"])
52 x = np.random.rand(numSources)*self.exposure.getWidth() + self.exposure.getX0()
53 y = np.random.rand(numSources)*self.exposure.getHeight() + self.exposure.getY0()
54 for _i in range(numSources):
55 source_test_centroid = lsst.geom.Point2D(x[_i], y[_i])
56 source = sourceCat.addNew()
57 source.set(centroidKey, source_test_centroid)
58 # All sources are unresolved.
59 source[self.unresolvedName] = 0.0
61 source_test_instFlux = 5.1
62 source_test_instFluxErr = 1e-3
64 for name in self.names:
65 sourceCat[name + "_instFlux"] = source_test_instFlux
66 sourceCat[name + "_instFluxErr"] = source_test_instFluxErr
67 sourceCat[name + "_flag"] = np.zeros(len(sourceCat), dtype=bool)
68 sourceCat[name + self.apNameStr + "_instFlux"] = source_test_instFlux * apCorrScale
69 sourceCat[name + self.apNameStr + "_instFluxErr"] = source_test_instFluxErr * apCorrScale
70 sourceCat[name + self.apNameStr + "_flag"] = np.zeros(len(sourceCat), dtype=bool)
72 return(sourceCat)
74 def setUp(self):
75 schema = afwTable.SourceTable.makeMinimalSchema()
76 apNameStr = "Ap"
77 config = measureApCorr.MeasureApCorrTask.ConfigClass()
78 unresolvedName = config.sourceSelector.active.unresolved.name
79 # Add fields in anti-sorted order to try to impose a need for sorting
80 # in the addition of the apCorr fields (may happen by fluke, but this
81 # is the best we can do to test this here.
82 names = ["test2", "test1"]
83 for name in names:
84 apName = name + apNameStr
85 addApCorrName(apName)
86 schema.addField(name + "_instFlux", type=float)
87 schema.addField(name + "_instFluxErr", type=float)
88 schema.addField(name + "_flag", type="Flag")
89 schema.addField(apName + "_instFlux", type=float)
90 schema.addField(apName + "_instFluxErr", type=float)
91 schema.addField(apName + "_flag", type="Flag")
92 schema.addField(names[0] + "_Centroid_x", type=float)
93 schema.addField(names[0] + "_Centroid_y", type=float)
94 schema.getAliasMap().set("slot_Centroid", names[0] + "_Centroid")
95 schema.addField(unresolvedName, type=float)
96 schema.addField("deblend_nChild", type=np.int32)
97 for flag in [
98 "base_PixelFlags_flag_edge",
99 "base_PixelFlags_flag_interpolatedCenter",
100 "base_PixelFlags_flag_saturatedCenter",
101 "base_PixelFlags_flag_crCenter",
102 "base_PixelFlags_flag_bad",
103 "base_PixelFlags_flag_interpolated",
104 "base_PixelFlags_flag_saturated",
105 ]:
106 schema.addField(flag, type="Flag")
107 config.refFluxName = names[0]
108 config.sourceSelector["science"].signalToNoise.fluxField = names[0] + "_instFlux"
109 config.sourceSelector["science"].signalToNoise.errField = names[0] + "_instFluxErr"
110 self.meas_apCorr_task = measureApCorr.MeasureApCorrTask(schema=schema, config=config)
111 self.names = names
112 self.apNameStr = apNameStr
113 self.schema = schema
114 self.exposure = lsst.afw.image.ExposureF(10, 10)
115 self.unresolvedName = unresolvedName
117 def tearDown(self):
118 del self.schema
119 del self.meas_apCorr_task
120 del self.exposure
122 def testAddFields(self):
123 """Instantiating the task should add one field to the schema."""
124 for name in self.names:
125 self.assertIn("apcorr_" + name + self.apNameStr + "_used", self.schema.getNames())
126 sortedNames = sorted(self.names)
127 key0 = self.schema.find("apcorr_" + sortedNames[0] + self.apNameStr + "_used").key
128 key1 = self.schema.find("apcorr_" + sortedNames[1] + self.apNameStr + "_used").key
129 # Check that the apCorr fields were added in a sorted order (not
130 # foolproof as this could have happened by fluke, but it's the best
131 # we can do to test this here (having added the two fields in an anti-
132 # sorted order).
133 self.assertLess(key0.getOffset() + key0.getBit(), key1.getOffset() + key1.getBit())
135 def testReturnApCorrMap(self):
136 """The measureApCorr task should return a structure with a single key 'apCorrMap'."""
137 struct = self.meas_apCorr_task.run(catalog=self.makeCatalog(), exposure=self.exposure)
138 self.assertEqual(list(struct.getDict().keys()), ['apCorrMap'])
140 def testApCorrMapKeys(self):
141 """An apCorrMap structure should have two keys per name supplied to addApCorrName()."""
142 key_names = []
143 for name in self.names:
144 apFluxName = name + self.apNameStr + "_instFlux"
145 apFluxErrName = name + self.apNameStr + "_instFluxErr"
146 struct = self.meas_apCorr_task.run(catalog=self.makeCatalog(), exposure=self.exposure)
147 key_names.append(apFluxName)
148 key_names.append(apFluxErrName)
149 self.assertEqual(set(struct.apCorrMap.keys()), set(key_names))
151 def testTooFewSources(self):
152 """ If there are too few sources, check that an exception is raised."""
153 # Create an empty catalog with no sources to process.
154 catalog = afwTable.SourceCatalog(self.schema)
155 with self.assertRaisesRegex(RuntimeError, "only 0 sources, but"):
156 self.meas_apCorr_task.run(catalog=catalog, exposure=self.exposure)
158 # We now try again after declaring that the aperture correction is
159 # allowed to fail. This should run cleanly without raising an exception.
160 for name in self.names:
161 self.meas_apCorr_task.config.allowFailure.append(name + self.apNameStr)
162 self.meas_apCorr_task.run(catalog=catalog, exposure=self.exposure)
164 def testSourceNotUsed(self):
165 """ Check that a source outside the bounding box is flagged as not used (False)."""
166 sourceCat = self.makeCatalog()
167 source = sourceCat.addNew()
168 nameAp = self.names[0] + "Ap"
169 source[nameAp + "_instFlux"] = 5.1
170 source[nameAp + "_instFluxErr"] = 1e-3
171 source[self.meas_apCorr_task.config.refFluxName + "_instFlux"] = 5.1
172 source[self.meas_apCorr_task.config.refFluxName + "_instFluxErr"] = 1e-3
173 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"])
174 source.set(centroidKey, lsst.geom.Point2D(15, 7.1))
175 apCorrFlagName = "apcorr_" + nameAp + "_used"
177 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
178 # Check that all but the final source are used.
179 self.assertTrue(sourceCat[apCorrFlagName][0: -1].all())
180 # Check that the final source is not used.
181 self.assertFalse(sourceCat[apCorrFlagName][-1])
183 def testSourceUsed(self):
184 """Check that valid sources inside the bounding box that are used have their flags set to True."""
185 sourceCat = self.makeCatalog()
186 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
187 for name in self.names:
188 self.assertTrue(sourceCat["apcorr_" + name + self.apNameStr + "_used"].all())
190 def testApertureMeasOnes(self):
191 """ Check that sources with aperture fluxes exactly the same as their catalog fluxes
192 returns an aperture correction map of 1s"""
193 apFluxName = self.names[0] + self.apNameStr + "_instFlux"
194 sourceCat = self.makeCatalog()
195 struct = self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
196 default_fill = apCorrDefaultMap(value=1.0, bbox=self.exposure.getBBox())
197 test_fill = afwImage.ImageF(self.exposure.getBBox())
198 struct.apCorrMap[apFluxName].fillImage(test_fill)
199 np.testing.assert_allclose(test_fill.getArray(), default_fill.getArray())
201 def testApertureMeasTens(self):
202 """Check that aperture correction scales source fluxes in the correct direction."""
203 apCorr_factor = 10.
204 sourceCat = self.makeCatalog(apCorrScale=apCorr_factor)
205 apFluxName = self.names[0] + self.apNameStr + "_instFlux"
206 struct = self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
207 default_fill = apCorrDefaultMap(value=apCorr_factor, bbox=self.exposure.getBBox())
208 test_fill = afwImage.ImageF(self.exposure.getBBox())
209 struct.apCorrMap[apFluxName].fillImage(test_fill)
210 np.testing.assert_allclose(test_fill.getArray(), default_fill.getArray())
212 def testFilterBadValue(self):
213 """Check that the aperture correction filters a bad value."""
214 sourceCat = self.makeCatalog()
215 source = sourceCat.addNew()
216 nameAp = self.names[0] + self.apNameStr
217 source[nameAp + "_instFlux"] = 100.0
218 source[nameAp + "_instFluxErr"] = 1e-3
219 source[self.meas_apCorr_task.config.refFluxName + "_instFlux"] = 5.1
220 source[self.meas_apCorr_task.config.refFluxName + "_instFluxErr"] = 1e-3
221 source[self.unresolvedName] = 0.0
222 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"])
223 x = self.exposure.getX0() + 1
224 y = self.exposure.getY0() + 1
225 source.set(centroidKey, lsst.geom.Point2D(x, y))
227 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
229 # Check that both Ap fluxes are removed as outliers; one is due
230 # to being unfilled (nan), the other is a large outlier.
231 for name in self.names:
232 apCorrFlagName = "apcorr_" + name + self.apNameStr + "_used"
233 # Check that all but the final source are used.
234 self.assertTrue(sourceCat[apCorrFlagName][0: -1].all())
235 # Check that the final source is not used.
236 self.assertFalse(sourceCat[apCorrFlagName][-1])
238 def testTooFewSourcesAfterFiltering(self):
239 """Check that the aperture correction fails when too many are filtered."""
240 sourceCat = self.makeCatalog()
241 self.meas_apCorr_task.config.minDegreesOfFreedom = 4
243 for name in self.names:
244 nameAp = name + self.apNameStr
245 sourceCat[nameAp + "_instFlux"][0] = 100.0
247 with self.assertRaisesRegex(RuntimeError, "only 4 sources remain"):
248 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
250 # We now try again after declaring that the aperture correction is
251 # allowed to fail. This should run cleanly without raising an exception.
252 for name in self.names:
253 self.meas_apCorr_task.config.allowFailure.append(name + self.apNameStr)
254 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
257class TestMemory(lsst.utils.tests.MemoryTestCase):
258 pass
261def setup_module(module):
262 lsst.utils.tests.init()
265if __name__ == "__main__": 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true
266 lsst.utils.tests.init()
267 unittest.main()