Coverage for tests / test_SdssCentroid.py: 18%
148 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:01 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 09:01 +0000
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
27from lsst.meas.base.tests import (AlgorithmTestCase, CentroidTransformTestCase,
28 SingleFramePluginTransformSetupHelper)
29import lsst.afw.geom
30import lsst.utils.tests
32# N.B. Some tests here depend on the noise realization in the test data
33# or from the numpy random number generator.
34# For the current test data and seed value, they pass, but they may not
35# if the test data is regenerated or the seed value changes. I've marked
36# these with an "rng dependent" comment. In most cases, they test that
37# the measured instFlux lies within 2 sigma of the correct value, which we
38# should expect to fail sometimes.
41class SdssCentroidTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase):
43 def setUp(self):
44 self.center = lsst.geom.Point2D(50.1, 49.8)
45 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-20, -30),
46 lsst.geom.Extent2I(140, 160))
47 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox)
48 self.dataset.addSource(100000.0, self.center)
50 def makeAlgorithm(self, ctrl=None):
51 """Construct an algorithm and return both it and its schema.
52 """
53 if ctrl is None:
54 ctrl = lsst.meas.base.SdssCentroidControl()
55 schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema()
56 algorithm = lsst.meas.base.SdssCentroidAlgorithm(ctrl, "base_SdssCentroid", schema)
57 return algorithm, schema
59 def testSingleFramePlugin(self):
60 """Test calling the algorithm through the plugin interface.
61 """
62 task = self.makeSingleFrameMeasurementTask("base_SdssCentroid")
63 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=0)
64 task.run(catalog, exposure)
65 record = catalog[0]
66 self.assertFalse(record.get("base_SdssCentroid_flag"))
67 self.assertFalse(record.get("base_SdssCentroid_flag_edge"))
68 self.assertFalse(record.get("base_SdssCentroid_flag_notAtMaximum"))
69 self.assertFalse(record.get("base_SdssCentroid_flag_near_edge"))
70 self.assertFalse(record.get("base_SdssCentroid_flag_resetToPeak"))
71 self.assertFalse(record.get("base_SdssCentroid_flag_badError"))
72 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_x"), record.get("truth_x"), rtol=0.005)
73 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_y"), record.get("truth_y"), rtol=0.005)
75 def testMonteCarlo(self):
76 """Test an ideal simulation, with no noise.
78 Demonstrate that:
80 - We get exactly the right answer, and
81 - The reported uncertainty agrees with a Monte Carlo test of the noise.
82 """
83 algorithm, schema = self.makeAlgorithm()
84 exposure, catalog = self.dataset.realize(0.0, schema, randomSeed=1)
85 record = catalog[0]
86 x = record.get("truth_x")
87 y = record.get("truth_y")
88 instFlux = record.get("truth_instFlux")
89 algorithm.measure(record, exposure)
90 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_x"), x, rtol=1E-4)
91 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_y"), y, rtol=1E-4)
92 for noise in (0.001, 0.01):
93 xList = []
94 yList = []
95 xErrList = []
96 yErrList = []
97 nSamples = 1000
98 for repeat in range(nSamples):
99 # By using ``repeat`` to seed the RNG, we get results which
100 # fall within the tolerances defined below. If we allow this
101 # test to be truly random, passing becomes RNG-dependent.
102 exposure, catalog = self.dataset.realize(noise*instFlux, schema, randomSeed=repeat)
103 record = catalog[0]
104 algorithm.measure(record, exposure)
105 xList.append(record.get("base_SdssCentroid_x"))
106 yList.append(record.get("base_SdssCentroid_y"))
107 xErrList.append(record.get("base_SdssCentroid_xErr"))
108 yErrList.append(record.get("base_SdssCentroid_yErr"))
109 xMean = np.mean(xList)
110 yMean = np.mean(yList)
111 xErrMean = np.mean(xErrList)
112 yErrMean = np.mean(yErrList)
113 xStandardDeviation = np.std(xList)
114 yStandardDeviation = np.std(yList)
115 self.assertFloatsAlmostEqual(xErrMean, xStandardDeviation, rtol=0.2) # rng dependent
116 self.assertFloatsAlmostEqual(yErrMean, yStandardDeviation, rtol=0.2) # rng dependent
117 self.assertLess(abs(xMean - x), 3.0*xErrMean / nSamples**0.5) # rng dependent
118 self.assertLess(abs(yMean - y), 3.0*yErrMean / nSamples**0.5) # rng dependent
120 def testEdge(self):
121 task = self.makeSingleFrameMeasurementTask("base_SdssCentroid")
122 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=2)
123 psfImage = exposure.getPsf().computeImage(self.center)
124 # construct a box that won't fit the full PSF model
125 bbox = psfImage.getBBox()
126 bbox.grow(-5)
127 subImage = lsst.afw.image.ExposureF(exposure, bbox)
128 # we also need to install a smaller footprint, or NoiseReplacer
129 # complains before we even get to measuring the centroid
130 record = catalog[0]
131 spanSet = lsst.afw.geom.SpanSet(bbox)
132 newFootprint = lsst.afw.detection.Footprint(spanSet)
133 peak = record.getFootprint().getPeaks()[0]
134 newFootprint.addPeak(peak.getFx(), peak.getFy(), peak.getPeakValue())
135 record.setFootprint(newFootprint)
136 # just measure the one object we've prepared for
137 task.measure(catalog, subImage)
138 self.assertTrue(record.get("base_SdssCentroid_flag"))
139 self.assertTrue(record.get("base_SdssCentroid_flag_edge"))
141 def testNearEdge(self):
142 task = self.makeSingleFrameMeasurementTask("base_SdssCentroid")
143 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=2)
144 psfImage = exposure.getPsf().computeImage(self.center)
145 # construct a box that won't fit the full PSF model
146 bbox = psfImage.getBBox()
147 bbox.grow(-3)
148 subImage = lsst.afw.image.ExposureF(exposure, bbox)
149 # we also need to install a smaller footprint, or NoiseReplacer
150 # complains before we even get to measuring the centroid
151 record = catalog[0]
152 spanSet = lsst.afw.geom.SpanSet(bbox)
153 newFootprint = lsst.afw.detection.Footprint(spanSet)
154 peak = record.getFootprint().getPeaks()[0]
155 newFootprint.addPeak(peak.getFx(), peak.getFy(), peak.getPeakValue())
156 record.setFootprint(newFootprint)
157 # just measure the one object we've prepared for
158 task.measure(catalog, subImage)
159 self.assertTrue(record.get("base_SdssCentroid_flag_near_edge"))
160 self.assertTrue(record.get("base_SdssCentroid_flag"))
162 def testNo2ndDerivative(self):
163 task = self.makeSingleFrameMeasurementTask("base_SdssCentroid")
164 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=3)
165 # cutout a subimage around object in the test image
166 bbox = lsst.geom.Box2I(lsst.geom.Point2I(self.center), lsst.geom.Extent2I(1, 1))
167 bbox.grow(20)
168 subImage = lsst.afw.image.ExposureF(exposure, bbox)
169 # A completely flat image will trigger the no 2nd derivative error
170 subImage.image.array[:] = 0
171 task.measure(catalog, subImage)
172 self.assertTrue(catalog[0].get("base_SdssCentroid_flag"))
173 self.assertTrue(catalog[0].get("base_SdssCentroid_flag_noSecondDerivative"))
175 def testNotAtMaximum(self):
176 task = self.makeSingleFrameMeasurementTask("base_SdssCentroid")
177 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=4)
178 # cutout a subimage around the object in the test image
179 bbox = lsst.geom.Box2I(lsst.geom.Point2I(self.center), lsst.geom.Extent2I(1, 1))
180 bbox.grow(20)
181 subImage = lsst.afw.image.ExposureF(exposure, bbox)
182 # zero out the central region, which will destroy the maximum
183 subImage.image.array[18:22, 18:22] = 0
184 task.measure(catalog, subImage)
185 self.assertTrue(catalog[0].get("base_SdssCentroid_flag"))
186 self.assertTrue(catalog[0].get("base_SdssCentroid_flag_notAtMaximum"))
188 def testNegative(self):
189 """Test that negative sources are well measured, without error flags.
190 """
191 dataset = lsst.meas.base.tests.TestDataset(self.bbox)
192 dataset.addSource(-10000.0, self.center, negative=True)
193 task = self.makeSingleFrameMeasurementTask("base_SdssCentroid")
194 exposure, catalog = dataset.realize(10.0, task.schema, randomSeed=4)
196 task.run(catalog, exposure)
197 record = catalog[0]
198 self.assertFalse(record.get("base_SdssCentroid_flag"))
199 self.assertFalse(record.get("base_SdssCentroid_flag_edge"))
200 self.assertFalse(record.get("base_SdssCentroid_flag_notAtMaximum"))
201 self.assertFalse(record.get("base_SdssCentroid_flag_near_edge"))
202 self.assertFalse(record.get("base_SdssCentroid_flag_resetToPeak"))
203 self.assertFalse(record.get("base_SdssCentroid_flag_badError"))
204 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_x"), record.get("truth_x"), rtol=0.005)
205 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_y"), record.get("truth_y"), rtol=0.005)
206 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_xErr"), 0.024, rtol=0.02)
207 self.assertFloatsAlmostEqual(record.get("base_SdssCentroid_yErr"), 0.024, rtol=0.02)
210class SdssCentroidTransformTestCase(CentroidTransformTestCase,
211 SingleFramePluginTransformSetupHelper,
212 lsst.utils.tests.TestCase):
213 controlClass = lsst.meas.base.SdssCentroidControl
214 algorithmClass = lsst.meas.base.SdssCentroidAlgorithm
215 transformClass = lsst.meas.base.SdssCentroidTransform
216 flagNames = ('flag', 'flag_edge', 'flag_badData')
217 singleFramePlugins = ('base_SdssCentroid',)
218 forcedPlugins = ('base_SdssCentroid',)
221class TestMemory(lsst.utils.tests.MemoryTestCase):
222 pass
225def setup_module(module):
226 lsst.utils.tests.init()
229if __name__ == "__main__": 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true
230 lsst.utils.tests.init()
231 unittest.main()