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