Coverage for tests/test_measureApCorr.py: 16%
175 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 03:08 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 03:08 -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
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.assertRaisesRegex(measureApCorr.MeasureApCorrError,
157 "Unable to measure aperture correction for 'test1Ap'"):
158 self.meas_apCorr_task.run(catalog=catalog, exposure=self.exposure)
160 # We now try again after declaring that the aperture correction is
161 # allowed to fail. This should run without raising an exception, but
162 # will log a warning.
163 for name in self.names:
164 self.meas_apCorr_task.config.allowFailure.append(name + self.apNameStr)
165 with self.assertLogs(level=logging.WARNING) as cm:
166 self.meas_apCorr_task.run(catalog=catalog, exposure=self.exposure)
167 self.assertIn("Unable to measure aperture correction for 'test1Ap'", cm.output[0])
169 def testSourceNotUsed(self):
170 """ Check that a source outside the bounding box is flagged as not used (False)."""
171 sourceCat = self.makeCatalog()
172 source = sourceCat.addNew()
173 nameAp = self.names[0] + "Ap"
174 source[nameAp + "_instFlux"] = 5.1
175 source[nameAp + "_instFluxErr"] = 1e-3
176 source[self.meas_apCorr_task.config.refFluxName + "_instFlux"] = 5.1
177 source[self.meas_apCorr_task.config.refFluxName + "_instFluxErr"] = 1e-3
178 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"])
179 source.set(centroidKey, lsst.geom.Point2D(15, 7.1))
180 apCorrFlagName = "apcorr_" + nameAp + "_used"
182 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
183 # Check that all but the final source are used.
184 self.assertTrue(sourceCat[apCorrFlagName][0: -1].all())
185 # Check that the final source is not used.
186 self.assertFalse(sourceCat[apCorrFlagName][-1])
188 def testSourceUsed(self):
189 """Check that valid sources inside the bounding box that are used have their flags set to True."""
190 sourceCat = self.makeCatalog()
191 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
192 for name in self.names:
193 self.assertTrue(sourceCat["apcorr_" + name + self.apNameStr + "_used"].all())
195 def testApertureMeasOnes(self):
196 """ Check that sources with aperture fluxes exactly the same as their catalog fluxes
197 returns an aperture correction map of 1s"""
198 apFluxName = self.names[0] + self.apNameStr + "_instFlux"
199 sourceCat = self.makeCatalog()
200 struct = self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
201 default_fill = apCorrDefaultMap(value=1.0, bbox=self.exposure.getBBox())
202 test_fill = afwImage.ImageF(self.exposure.getBBox())
203 struct.apCorrMap[apFluxName].fillImage(test_fill)
204 np.testing.assert_allclose(test_fill.getArray(), default_fill.getArray())
206 def testApertureMeasTens(self):
207 """Check that aperture correction scales source fluxes in the correct direction."""
208 apCorr_factor = 10.
209 sourceCat = self.makeCatalog(apCorrScale=apCorr_factor)
210 apFluxName = self.names[0] + self.apNameStr + "_instFlux"
211 struct = self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
212 default_fill = apCorrDefaultMap(value=apCorr_factor, bbox=self.exposure.getBBox())
213 test_fill = afwImage.ImageF(self.exposure.getBBox())
214 struct.apCorrMap[apFluxName].fillImage(test_fill)
215 np.testing.assert_allclose(test_fill.getArray(), default_fill.getArray())
217 def testFilterBadValue(self):
218 """Check that the aperture correction filters a bad value."""
219 sourceCat = self.makeCatalog()
220 source = sourceCat.addNew()
221 nameAp = self.names[0] + self.apNameStr
222 source[nameAp + "_instFlux"] = 100.0
223 source[nameAp + "_instFluxErr"] = 1e-3
224 source[self.meas_apCorr_task.config.refFluxName + "_instFlux"] = 5.1
225 source[self.meas_apCorr_task.config.refFluxName + "_instFluxErr"] = 1e-3
226 source[self.unresolvedName] = 0.0
227 centroidKey = afwTable.Point2DKey(self.schema["slot_Centroid"])
228 x = self.exposure.getX0() + 1
229 y = self.exposure.getY0() + 1
230 source.set(centroidKey, lsst.geom.Point2D(x, y))
232 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
234 # Check that both Ap fluxes are removed as outliers; one is due
235 # to being unfilled (nan), the other is a large outlier.
236 for name in self.names:
237 apCorrFlagName = "apcorr_" + name + self.apNameStr + "_used"
238 # Check that all but the final source are used.
239 self.assertTrue(sourceCat[apCorrFlagName][0: -1].all())
240 # Check that the final source is not used.
241 self.assertFalse(sourceCat[apCorrFlagName][-1])
243 def testTooFewSourcesAfterFiltering(self):
244 """Check that the aperture correction fails when too many are filtered."""
245 sourceCat = self.makeCatalog()
246 self.meas_apCorr_task.config.minDegreesOfFreedom = 4
248 for name in self.names:
249 nameAp = name + self.apNameStr
250 sourceCat[nameAp + "_instFlux"][0] = 100.0
252 with self.assertRaisesRegex(measureApCorr.MeasureApCorrError,
253 f"Unable to measure aperture correction for '{nameAp}'"):
254 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
256 # We now try again after declaring that the aperture correction is
257 # allowed to fail. This should run cleanly without raising an exception.
258 for name in self.names:
259 self.meas_apCorr_task.config.allowFailure.append(name + self.apNameStr)
260 self.meas_apCorr_task.run(catalog=sourceCat, exposure=self.exposure)
263class TestMemory(lsst.utils.tests.MemoryTestCase):
264 pass
267def setup_module(module):
268 lsst.utils.tests.init()
271if __name__ == "__main__": 271 ↛ 272line 271 didn't jump to line 272, because the condition on line 271 was never true
272 lsst.utils.tests.init()
273 unittest.main()