Coverage for tests/test_ApertureFlux.py: 18%
155 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 03:47 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 03:47 -0700
1# This file is part of meas_base.
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
24import numpy as np
26import lsst.geom
27import lsst.afw.geom
28import lsst.afw.image
29import lsst.utils.tests
30from lsst.meas.base import ApertureFluxAlgorithm
31from lsst.meas.base.tests import (AlgorithmTestCase, FluxTransformTestCase,
32 SingleFramePluginTransformSetupHelper)
35class ApertureFluxTestCase(lsst.utils.tests.TestCase):
36 """Test case for the ApertureFlux algorithm base class.
37 """
39 def setUp(self):
40 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(20, -100), lsst.geom.Point2I(100, -20))
41 self.exposure = lsst.afw.image.ExposureF(self.bbox)
42 self.exposure.getMaskedImage().getImage().set(1.0)
43 self.exposure.getMaskedImage().getVariance().set(0.25)
44 self.ctrl = ApertureFluxAlgorithm.Control()
46 def tearDown(self):
47 del self.bbox
48 del self.exposure
50 def computeNaiveArea(self, position, radius):
51 """Computes the area of a circular aperture.
53 Calculates the area of the aperture by the "naive" approach of testing
54 each pixel to see whether its center lies within the aperture.
55 """
56 x, y = np.meshgrid(np.arange(self.bbox.getBeginX(), self.bbox.getEndX()),
57 np.arange(self.bbox.getBeginY(), self.bbox.getEndY()))
58 return ((x - position.getX())**2 + (y - position.getY())**2 <= radius**2).sum()
60 def testNaive(self):
61 positions = [lsst.geom.Point2D(60.0, -60.0),
62 lsst.geom.Point2D(60.5, -60.0),
63 lsst.geom.Point2D(60.0, -60.5),
64 lsst.geom.Point2D(60.5, -60.5)]
65 radii = [12.0, 17.0]
66 for position in positions:
67 for radius in radii:
68 ellipse = lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(radius, radius, 0.0), position)
69 area = self.computeNaiveArea(position, radius)
70 # test that this isn't the same as the sinc instFlux
71 self.assertFloatsNotEqual(
72 ApertureFluxAlgorithm.computeSincFlux(self.exposure.getMaskedImage().getImage(),
73 ellipse, self.ctrl).instFlux, area)
75 def check(method, image):
76 """Test that all instFlux measurement invocations work.
78 That is, that they return the expected value.
79 """
80 result = method(image, ellipse, self.ctrl)
81 self.assertFloatsAlmostEqual(result.instFlux, area)
82 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number))
83 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number))
84 if hasattr(image, "getVariance"):
85 self.assertFloatsAlmostEqual(result.instFluxErr, (area*0.25)**0.5)
86 else:
87 self.assertTrue(np.isnan(result.instFluxErr))
88 check(ApertureFluxAlgorithm.computeNaiveFlux, self.exposure.getMaskedImage())
89 check(ApertureFluxAlgorithm.computeNaiveFlux, self.exposure.getMaskedImage().getImage())
90 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage())
91 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage().getImage())
92 # test failure conditions when the aperture itself is truncated
93 invalid = ApertureFluxAlgorithm.computeNaiveFlux(
94 self.exposure.getMaskedImage().getImage(),
95 lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(12.0, 12.0),
96 lsst.geom.Point2D(25.0, -60.0)),
97 self.ctrl)
98 self.assertTrue(invalid.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number))
99 self.assertFalse(invalid.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number))
100 self.assertTrue(np.isnan(invalid.instFlux))
102 def testSinc(self):
103 positions = [lsst.geom.Point2D(60.0, -60.0),
104 lsst.geom.Point2D(60.5, -60.0),
105 lsst.geom.Point2D(60.0, -60.5),
106 lsst.geom.Point2D(60.5, -60.5)]
107 radii = [7.0, 9.0]
108 for position in positions:
109 for radius in radii:
110 ellipse = lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(radius, radius, 0.0), position)
111 area = ellipse.getCore().getArea()
112 # test that this isn't the same as the naive instFlux
113 self.assertFloatsNotEqual(
114 ApertureFluxAlgorithm.computeNaiveFlux(self.exposure.getMaskedImage().getImage(),
115 ellipse, self.ctrl).instFlux, area)
117 def check(method, image):
118 # test that all the ways we could invoke sinc flux
119 # measurement produce the expected result
120 result = method(image, ellipse, self.ctrl)
121 self.assertFloatsAlmostEqual(result.instFlux, area, rtol=1E-3)
122 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number))
123 self.assertFalse(result.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number))
124 if hasattr(image, "getVariance"):
125 self.assertFalse(np.isnan(result.instFluxErr))
126 else:
127 self.assertTrue(np.isnan(result.instFluxErr))
128 check(ApertureFluxAlgorithm.computeSincFlux, self.exposure.getMaskedImage())
129 check(ApertureFluxAlgorithm.computeSincFlux, self.exposure.getMaskedImage().getImage())
130 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage())
131 check(ApertureFluxAlgorithm.computeFlux, self.exposure.getMaskedImage().getImage())
132 # test failure conditions when the aperture itself is truncated
133 invalid1 = ApertureFluxAlgorithm.computeSincFlux(
134 self.exposure.getMaskedImage().getImage(),
135 lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(9.0, 9.0), lsst.geom.Point2D(25.0, -60.0)),
136 self.ctrl)
137 self.assertTrue(invalid1.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number))
138 self.assertTrue(invalid1.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number))
139 self.assertTrue(np.isnan(invalid1.instFlux))
140 # test failure conditions when the aperture is not truncated, but the
141 # sinc coeffs are
142 invalid2 = ApertureFluxAlgorithm.computeSincFlux(
143 self.exposure.getMaskedImage().getImage(),
144 lsst.afw.geom.Ellipse(lsst.afw.geom.ellipses.Axes(9.0, 9.0), lsst.geom.Point2D(30.0, -60.0)),
145 self.ctrl)
146 self.assertFalse(invalid2.getFlag(ApertureFluxAlgorithm.APERTURE_TRUNCATED.number))
147 self.assertTrue(invalid2.getFlag(ApertureFluxAlgorithm.SINC_COEFFS_TRUNCATED.number))
148 self.assertFalse(np.isnan(invalid2.instFlux))
151class CircularApertureFluxTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase):
152 """Test case for the CircularApertureFlux algorithm/plugin.
153 """
155 def setUp(self):
156 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
157 lsst.geom.Extent2I(100, 100))
158 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox)
159 # first source is a point
160 self.dataset.addSource(100000.0, lsst.geom.Point2D(49.5, 49.5))
162 def tearDown(self):
163 del self.bbox
164 del self.dataset
166 def testSingleFramePlugin(self):
167 baseName = "base_CircularApertureFlux"
168 config = self.makeSingleFrameMeasurementConfig(baseName)
169 config.plugins[baseName].maxSincRadius = 20
170 ctrl = config.plugins[baseName].makeControl()
171 algMetadata = lsst.daf.base.PropertyList()
172 task = self.makeSingleFrameMeasurementTask(config=config, algMetadata=algMetadata)
173 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=0)
174 task.run(catalog, exposure)
175 radii = algMetadata.getArray("%s_RADII" % (baseName.upper(),))
176 self.assertEqual(list(radii), list(ctrl.radii))
177 for record in catalog:
178 lastFlux = 0.0
179 lastFluxErr = 0.0
180 for n, radius in enumerate(radii):
181 # Test that the flags are what we expect
182 prefix = ApertureFluxAlgorithm.makeFieldPrefix(baseName, radius)
183 if radius <= ctrl.maxSincRadius:
184 self.assertFalse(record.get(record.schema.join(prefix, "flag")))
185 self.assertFalse(record.get(record.schema.join(prefix, "flag_apertureTruncated")))
186 self.assertEqual(
187 record.get(record.schema.join(prefix, "flag_sincCoeffsTruncated")),
188 radius > 12
189 )
190 else:
191 self.assertTrue(record.schema.join(prefix, "flag_sincCoeffsTruncated")
192 not in record.getSchema())
193 self.assertEqual(record.get(record.schema.join(prefix, "flag")), radius >= 50)
194 self.assertEqual(record.get(record.schema.join(prefix, "flag_apertureTruncated")),
195 radius >= 50)
196 # Test that the instFluxes and uncertainties increase as we
197 # increase the apertures, or that they match the true instFlux
198 # within 3 sigma. This is just a test as to whether the
199 # values are reasonable. As to whether the values are exactly
200 # correct, we rely on the tests on ApertureFluxAlgorithm's
201 # static methods, as the way the plugins code calls that is
202 # extremely simple, so if the results we get are reasonable,
203 # it's hard to imagine how they could be incorrect if
204 # ApertureFluxAlgorithm's tests are valid.
205 currentFlux = record.get(record.schema.join(prefix, "instFlux"))
206 currentFluxErr = record.get(record.schema.join(prefix, "instFluxErr"))
207 if not record.get(record.schema.join(prefix, "flag")):
208 self.assertTrue(currentFlux > lastFlux
209 or (record.get("truth_instFlux") - currentFlux) < 3*currentFluxErr)
210 self.assertGreater(currentFluxErr, lastFluxErr)
211 lastFlux = currentFlux
212 lastFluxErr = currentFluxErr
213 else:
214 self.assertTrue(np.isnan(currentFlux))
215 self.assertTrue(np.isnan(currentFluxErr))
216 # When measuring an isolated point source with a sufficiently
217 # large aperture, we should recover the known input instFlux.
218 if record.get("truth_isStar") and record.get("parent") == 0:
219 self.assertFloatsAlmostEqual(record.get("base_CircularApertureFlux_25_0_instFlux"),
220 record.get("truth_instFlux"), rtol=0.02)
222 def testForcedPlugin(self):
223 baseName = "base_CircularApertureFlux"
224 algMetadata = lsst.daf.base.PropertyList()
225 task = self.makeForcedMeasurementTask(baseName, algMetadata=algMetadata)
226 radii = algMetadata.getArray("%s_RADII" % (baseName.upper(),))
227 measWcs = self.dataset.makePerturbedWcs(self.dataset.exposure.getWcs(), randomSeed=1)
228 measDataset = self.dataset.transform(measWcs)
229 exposure, truthCatalog = measDataset.realize(10.0, measDataset.makeMinimalSchema(), randomSeed=1)
230 refCat = self.dataset.catalog
231 refWcs = self.dataset.exposure.getWcs()
232 measCat = task.generateMeasCat(exposure, refCat, refWcs)
233 task.attachTransformedFootprints(measCat, refCat, exposure, refWcs)
234 task.run(measCat, exposure, refCat, refWcs)
235 for measRecord, truthRecord in zip(measCat, truthCatalog):
236 # Centroid tolerances set to ~ single precision epsilon
237 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_x"),
238 truthRecord.get("truth_x"), rtol=1E-7)
239 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_y"),
240 truthRecord.get("truth_y"), rtol=1E-7)
241 for n, radius in enumerate(radii):
242 prefix = ApertureFluxAlgorithm.makeFieldPrefix(baseName, radius)
243 self.assertFalse(measRecord.get(measRecord.schema.join(prefix, "flag")))
244 # CircularApertureFlux isn't designed to do a good job in
245 # forced mode, because it doesn't account for changes in the
246 # PSF or changes in the WCS. Hence, this is really just a
247 # test to make sure the values are reasonable and that it runs
248 # with no unexpected errors.
249 self.assertFloatsAlmostEqual(measRecord.get(measRecord.schema.join(prefix, "instFlux")),
250 truthCatalog.get("truth_instFlux"), rtol=1.0)
251 self.assertLess(measRecord.get(measRecord.schema.join(prefix, "instFluxErr")), (n+1)*150.0)
254class ApertureFluxTransformTestCase(FluxTransformTestCase, SingleFramePluginTransformSetupHelper,
255 lsst.utils.tests.TestCase):
257 class CircApFluxAlgorithmFactory:
258 """Supply an empty ``PropertyList`` to `CircularApertureFluxAlgorithm`.
260 This is a helper class to make testing more convenient.
261 """
263 def __call__(self, control, name, inputSchema):
264 return lsst.meas.base.CircularApertureFluxAlgorithm(control, name, inputSchema,
265 lsst.daf.base.PropertyList())
267 controlClass = lsst.meas.base.ApertureFluxAlgorithm.Control
268 algorithmClass = CircApFluxAlgorithmFactory()
269 transformClass = lsst.meas.base.ApertureFluxTransform
270 flagNames = ('flag', 'flag_apertureTruncated', 'flag_sincCoeffsTruncated')
271 singleFramePlugins = ('base_CircularApertureFlux',)
272 forcedPlugins = ('base_CircularApertureFlux',)
274 def testTransform(self):
275 """Test `ApertureFluxTransform` with a synthetic catalog.
276 """
277 FluxTransformTestCase.testTransform(self, [ApertureFluxAlgorithm.makeFieldPrefix(self.name, r)
278 for r in self.control.radii])
281class TestMemory(lsst.utils.tests.MemoryTestCase):
282 pass
285def setup_module(module):
286 lsst.utils.tests.init()
289if __name__ == "__main__": 289 ↛ 290line 289 didn't jump to line 290, because the condition on line 289 was never true
290 lsst.utils.tests.init()
291 unittest.main()