Coverage for tests / test_SdssCentroid.py: 18%

148 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 08:30 +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/>. 

21 

22import unittest 

23 

24import numpy as np 

25 

26import lsst.geom 

27from lsst.meas.base.tests import (AlgorithmTestCase, CentroidTransformTestCase, 

28 SingleFramePluginTransformSetupHelper) 

29import lsst.afw.geom 

30import lsst.utils.tests 

31 

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. 

39 

40 

41class SdssCentroidTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

42 

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) 

49 

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 

58 

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) 

74 

75 def testMonteCarlo(self): 

76 """Test an ideal simulation, with no noise. 

77 

78 Demonstrate that: 

79 

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 

119 

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")) 

140 

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")) 

161 

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")) 

174 

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")) 

187 

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) 

195 

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) 

208 

209 

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',) 

219 

220 

221class TestMemory(lsst.utils.tests.MemoryTestCase): 

222 pass 

223 

224 

225def setup_module(module): 

226 lsst.utils.tests.init() 

227 

228 

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()