Coverage for tests/test_PsfFlux.py: 20%

156 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-21 03:08 -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/>. 

21 

22import unittest 

23 

24import numpy as np 

25 

26import lsst.geom 

27import lsst.afw.image 

28import lsst.afw.table 

29import lsst.utils.tests 

30 

31from lsst.meas.base.tests import (AlgorithmTestCase, FluxTransformTestCase, 

32 SingleFramePluginTransformSetupHelper) 

33 

34 

35def compute_chi2(exposure, centroid, instFlux, maskPlane=None): 

36 """Return the chi2 for the exposure PSF at the given position. 

37 

38 Parameters 

39 ---------- 

40 exposure : `lsst.afw.image.Exposure` 

41 Exposure to calculate the PSF chi2 on. 

42 centroid : `lsst::geom::Point2D` 

43 Center of the source on the exposure to calculate the PSF at. 

44 instFlux : `float` 

45 Total flux of this source, as computed by the fitting algorithm. 

46 maskPlane : `lsst.afw.image.Image`, optional 

47 Pixels to mask out of the calculation. 

48 

49 Returns 

50 ------- 

51 chi2, nPixels: `float`, `float` 

52 The computed chi2 and number of pixels included in the calculation. 

53 """ 

54 psfImage = exposure.getPsf().computeImage(centroid) 

55 scaledPsf = psfImage.array*instFlux 

56 # Get a sub-image the same size as the returned PSF image. 

57 sub = exposure.Factory(exposure, psfImage.getBBox(), lsst.afw.image.LOCAL) 

58 if maskPlane is not None: 

59 unmasked = np.logical_not(sub.mask.array & sub.mask.getPlaneBitMask(maskPlane)) 

60 chi2 = np.sum((sub.image.array[unmasked] - scaledPsf[unmasked])**2 

61 / sub.variance.array[unmasked]) 

62 nPixels = unmasked.sum() 

63 else: 

64 chi2 = np.sum((sub.image.array - scaledPsf)**2 / sub.variance.array) 

65 nPixels = np.prod(sub.mask.array.shape) 

66 return chi2, nPixels 

67 

68 

69class PsfFluxTestCase(AlgorithmTestCase, lsst.utils.tests.TestCase): 

70 

71 def setUp(self): 

72 self.center = lsst.geom.Point2D(50.1, 49.8) 

73 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

74 lsst.geom.Extent2I(100, 100)) 

75 self.dataset = lsst.meas.base.tests.TestDataset(self.bbox) 

76 self.dataset.addSource(100000.0, self.center) 

77 

78 def tearDown(self): 

79 del self.center 

80 del self.bbox 

81 del self.dataset 

82 

83 def makeAlgorithm(self, ctrl=None): 

84 """Construct an algorithm and return both it and its schema. 

85 """ 

86 if ctrl is None: 

87 ctrl = lsst.meas.base.PsfFluxControl() 

88 schema = lsst.meas.base.tests.TestDataset.makeMinimalSchema() 

89 algorithm = lsst.meas.base.PsfFluxAlgorithm(ctrl, "base_PsfFlux", schema) 

90 return algorithm, schema 

91 

92 def testMasking(self): 

93 algorithm, schema = self.makeAlgorithm() 

94 # Results are RNG dependent; we choose a seed that is known to pass. 

95 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=0) 

96 record = catalog[0] 

97 badPoint = lsst.geom.Point2I(self.center) + lsst.geom.Extent2I(3, 4) 

98 imageArray = exposure.getMaskedImage().getImage().getArray() 

99 maskArray = exposure.getMaskedImage().getMask().getArray() 

100 badMask = exposure.getMaskedImage().getMask().getPlaneBitMask("BAD") 

101 imageArray[badPoint.getY() - exposure.getY0(), badPoint.getX() - exposure.getX0()] = np.inf 

102 maskArray[badPoint.getY() - exposure.getY0(), badPoint.getX() - exposure.getX0()] |= badMask 

103 # Should get an infinite value exception, because we didn't mask that 

104 # one pixel 

105 with self.assertRaises(lsst.meas.base.PixelValueError): 

106 algorithm.measure(record, exposure) 

107 # If we do mask it, we should get a reasonable result 

108 ctrl = lsst.meas.base.PsfFluxControl() 

109 ctrl.badMaskPlanes = ["BAD"] 

110 algorithm, schema = self.makeAlgorithm(ctrl) 

111 algorithm.measure(record, exposure) 

112 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), 

113 record.get("truth_instFlux"), 

114 atol=3*record.get("base_PsfFlux_instFluxErr")) 

115 chi2, nPixels = compute_chi2(exposure, record.getCentroid(), 

116 record.get("base_PsfFlux_instFlux"), 

117 "BAD") 

118 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_chi2"), chi2, rtol=1e-7) 

119 # If we mask the whole image, we should get a failure flag. 

120 maskArray[:, :] |= badMask 

121 algorithm.measure(record, exposure) 

122 self.assertEqual(record.get("base_PsfFlux_flag_noGoodPixels"), 1) 

123 self.assertEqual(record.get("base_PsfFlux_npixels"), nPixels) 

124 

125 def testSubImage(self): 

126 """Test measurement on sub-images. 

127 

128 Specifically, checks that we don't get confused by images with nonzero 

129 ``xy0``, and that the ``EDGE`` flag is set when it should be. 

130 """ 

131 

132 algorithm, schema = self.makeAlgorithm() 

133 # Results are RNG dependent; we choose a seed that is known to pass. 

134 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=1) 

135 record = catalog[0] 

136 psfImage = exposure.getPsf().computeImage(record.getCentroid()) 

137 bbox = psfImage.getBBox() 

138 bbox.grow(-1) 

139 subExposure = exposure.Factory(exposure, bbox, lsst.afw.image.LOCAL) 

140 

141 algorithm.measure(record, subExposure) 

142 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), record.get("truth_instFlux"), 

143 atol=3*record.get("base_PsfFlux_instFluxErr")) 

144 self.assertTrue(record.get("base_PsfFlux_flag_edge")) 

145 

146 # Calculating chi2 requires trimming the PSF image by one pixel per side 

147 # to match the subExposure created above, so we can't use compute_chi2() 

148 # directly here. 

149 scaledPsf = psfImage.array[1:-1, 1:-1]*record.get("base_PsfFlux_instFlux") 

150 chi2 = np.sum((subExposure.image.array - scaledPsf)**2 / subExposure.variance.array) 

151 nPixels = np.prod(subExposure.image.array.shape) 

152 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_chi2"), chi2, rtol=1e-7) 

153 self.assertEqual(record.get("base_PsfFlux_npixels"), nPixels) 

154 

155 def testNoPsf(self): 

156 """Test that we raise `FatalAlgorithmError` when there's no PSF. 

157 """ 

158 algorithm, schema = self.makeAlgorithm() 

159 # Results are RNG dependent; we choose a seed that is known to pass. 

160 exposure, catalog = self.dataset.realize(10.0, schema, randomSeed=2) 

161 exposure.setPsf(None) 

162 with self.assertRaises(lsst.meas.base.FatalAlgorithmError): 

163 algorithm.measure(catalog[0], exposure) 

164 

165 def testMonteCarlo(self): 

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

167 

168 Demonstrate that: 

169 

170 - We get exactly the right answer, and 

171 - The reported uncertainty agrees with a Monte Carlo test of the noise. 

172 """ 

173 algorithm, schema = self.makeAlgorithm() 

174 # Results are RNG dependent; we choose a seed that is known to pass. 

175 exposure, catalog = self.dataset.realize(0.0, schema, randomSeed=3) 

176 record = catalog[0] 

177 instFlux = record.get("truth_instFlux") 

178 algorithm.measure(record, exposure) 

179 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), instFlux, rtol=1E-3) 

180 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFluxErr"), 0.0, rtol=1E-3) 

181 # no noise, so infinite chi2 on this one 

182 self.assertEqual(record.get("base_PsfFlux_chi2"), np.inf) 

183 for noise in (0.001, 0.01, 0.1): 

184 nSamples = 1000 

185 instFluxes = np.zeros(nSamples, dtype=np.float64) 

186 instFluxErrs = np.zeros(nSamples, dtype=np.float64) 

187 expectedChi2s = np.zeros(nSamples, dtype=np.float64) 

188 expectedNPixels = -100 

189 measuredChi2s = np.zeros(nSamples, dtype=np.float64) 

190 measuredNPixels = np.zeros(nSamples, dtype=np.float64) 

191 for i in range(nSamples): 

192 # By using ``i`` to seed the RNG, we get results which 

193 # fall within the tolerances defined below. If we allow this 

194 # test to be truly random, passing becomes RNG-dependent. 

195 exposure, catalog = self.dataset.realize(noise*instFlux, schema, randomSeed=i) 

196 record = catalog[0] 

197 algorithm.measure(record, exposure) 

198 instFluxes[i] = record.get("base_PsfFlux_instFlux") 

199 instFluxErrs[i] = record.get("base_PsfFlux_instFluxErr") 

200 measuredChi2s[i] = record.get("base_PsfFlux_chi2") 

201 measuredNPixels[i] = record.get("base_PsfFlux_npixels") 

202 chi2, nPixels = compute_chi2(exposure, record.getCentroid(), instFluxes[i]) 

203 expectedChi2s[i] = chi2 

204 expectedNPixels = nPixels # should be the same each time 

205 instFluxMean = np.mean(instFluxes) 

206 instFluxErrMean = np.mean(instFluxErrs) 

207 instFluxStandardDeviation = np.std(instFluxes) 

208 self.assertFloatsAlmostEqual(instFluxErrMean, instFluxStandardDeviation, rtol=0.10) 

209 self.assertLess(abs(instFluxMean - instFlux), 2.0*instFluxErrMean / nSamples**0.5) 

210 self.assertFloatsAlmostEqual(measuredChi2s, expectedChi2s, rtol=1e-7) 

211 # Should have the exact same number of pixels used every time. 

212 self.assertTrue(np.all(measuredNPixels == expectedNPixels)) 

213 

214 def testSingleFramePlugin(self): 

215 task = self.makeSingleFrameMeasurementTask("base_PsfFlux") 

216 # Results are RNG dependent; we choose a seed that is known to pass. 

217 exposure, catalog = self.dataset.realize(10.0, task.schema, randomSeed=4) 

218 task.run(catalog, exposure) 

219 record = catalog[0] 

220 self.assertFalse(record.get("base_PsfFlux_flag")) 

221 self.assertFalse(record.get("base_PsfFlux_flag_noGoodPixels")) 

222 self.assertFalse(record.get("base_PsfFlux_flag_edge")) 

223 self.assertFloatsAlmostEqual(record.get("base_PsfFlux_instFlux"), record.get("truth_instFlux"), 

224 atol=3*record.get("base_PsfFlux_instFluxErr")) 

225 

226 def testForcedPlugin(self): 

227 task = self.makeForcedMeasurementTask("base_PsfFlux") 

228 # Results of this test are RNG dependent: we choose seeds that are 

229 # known to pass. 

230 measWcs = self.dataset.makePerturbedWcs(self.dataset.exposure.getWcs(), randomSeed=5) 

231 measDataset = self.dataset.transform(measWcs) 

232 exposure, truthCatalog = measDataset.realize(10.0, measDataset.makeMinimalSchema(), randomSeed=5) 

233 refCat = self.dataset.catalog 

234 refWcs = self.dataset.exposure.getWcs() 

235 measCat = task.generateMeasCat(exposure, refCat, refWcs) 

236 task.attachTransformedFootprints(measCat, refCat, exposure, refWcs) 

237 task.run(measCat, exposure, refCat, refWcs) 

238 measRecord = measCat[0] 

239 truthRecord = truthCatalog[0] 

240 # Centroid tolerances set to ~ single precision epsilon 

241 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_x"), 

242 truthRecord.get("truth_x"), rtol=1E-7) 

243 self.assertFloatsAlmostEqual(measRecord.get("slot_Centroid_y"), 

244 truthRecord.get("truth_y"), rtol=1E-7) 

245 self.assertFalse(measRecord.get("base_PsfFlux_flag")) 

246 self.assertFalse(measRecord.get("base_PsfFlux_flag_noGoodPixels")) 

247 self.assertFalse(measRecord.get("base_PsfFlux_flag_edge")) 

248 self.assertFloatsAlmostEqual(measRecord.get("base_PsfFlux_instFlux"), 

249 truthCatalog.get("truth_instFlux"), rtol=1E-3) 

250 self.assertLess(measRecord.get("base_PsfFlux_instFluxErr"), 500.0) 

251 

252 

253class PsfFluxTransformTestCase(FluxTransformTestCase, SingleFramePluginTransformSetupHelper, 

254 lsst.utils.tests.TestCase): 

255 controlClass = lsst.meas.base.PsfFluxControl 

256 algorithmClass = lsst.meas.base.PsfFluxAlgorithm 

257 transformClass = lsst.meas.base.PsfFluxTransform 

258 flagNames = ('flag', 'flag_noGoodPixels', 'flag_edge') 

259 singleFramePlugins = ('base_PsfFlux',) 

260 forcedPlugins = ('base_PsfFlux',) 

261 

262 

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

264 pass 

265 

266 

267def setup_module(module): 

268 lsst.utils.tests.init() 

269 

270 

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