Coverage for tests/test_PsfFlux.py: 20%

157 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 03:58 -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 MeasurementError 

120 maskArray[:, :] |= badMask 

121 with self.assertRaises(lsst.meas.base.MeasurementError) as context: 

122 algorithm.measure(record, exposure) 

123 self.assertEqual(context.exception.getFlagBit(), 

124 lsst.meas.base.PsfFluxAlgorithm.NO_GOOD_PIXELS.number) 

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

126 

127 def testSubImage(self): 

128 """Test measurement on sub-images. 

129 

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

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

132 """ 

133 

134 algorithm, schema = self.makeAlgorithm() 

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

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

137 record = catalog[0] 

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

139 bbox = psfImage.getBBox() 

140 bbox.grow(-1) 

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

142 

143 algorithm.measure(record, subExposure) 

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

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

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

147 

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

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

150 # directly here. 

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

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

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

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

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

156 

157 def testNoPsf(self): 

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

159 """ 

160 algorithm, schema = self.makeAlgorithm() 

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

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

163 exposure.setPsf(None) 

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

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

166 

167 def testMonteCarlo(self): 

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

169 

170 Demonstrate that: 

171 

172 - We get exactly the right answer, and 

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

174 """ 

175 algorithm, schema = self.makeAlgorithm() 

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

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

178 record = catalog[0] 

179 instFlux = record.get("truth_instFlux") 

180 algorithm.measure(record, exposure) 

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

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

183 # no noise, so infinite chi2 on this one 

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

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

186 nSamples = 1000 

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

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

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

190 expectedNPixels = -100 

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

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

193 for i in range(nSamples): 

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

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

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

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

198 record = catalog[0] 

199 algorithm.measure(record, exposure) 

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

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

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

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

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

205 expectedChi2s[i] = chi2 

206 expectedNPixels = nPixels # should be the same each time 

207 instFluxMean = np.mean(instFluxes) 

208 instFluxErrMean = np.mean(instFluxErrs) 

209 instFluxStandardDeviation = np.std(instFluxes) 

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

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

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

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

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

215 

216 def testSingleFramePlugin(self): 

217 task = self.makeSingleFrameMeasurementTask("base_PsfFlux") 

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

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

220 task.run(catalog, exposure) 

221 record = catalog[0] 

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

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

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

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

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

227 

228 def testForcedPlugin(self): 

229 task = self.makeForcedMeasurementTask("base_PsfFlux") 

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

231 # known to pass. 

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

233 measDataset = self.dataset.transform(measWcs) 

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

235 refCat = self.dataset.catalog 

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

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

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

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

240 measRecord = measCat[0] 

241 truthRecord = truthCatalog[0] 

242 # Centroid tolerances set to ~ single precision epsilon 

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

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

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

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

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

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

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

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

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

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

253 

254 

255class PsfFluxTransformTestCase(FluxTransformTestCase, SingleFramePluginTransformSetupHelper, 

256 lsst.utils.tests.TestCase): 

257 controlClass = lsst.meas.base.PsfFluxControl 

258 algorithmClass = lsst.meas.base.PsfFluxAlgorithm 

259 transformClass = lsst.meas.base.PsfFluxTransform 

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

261 singleFramePlugins = ('base_PsfFlux',) 

262 forcedPlugins = ('base_PsfFlux',) 

263 

264 

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

266 pass 

267 

268 

269def setup_module(module): 

270 lsst.utils.tests.init() 

271 

272 

273if __name__ == "__main__": 273 ↛ 274line 273 didn't jump to line 274, because the condition on line 273 was never true

274 lsst.utils.tests.init() 

275 unittest.main()