Coverage for tests/test_MeasureSources.py: 12%

212 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-10 10:37 +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 math 

23import unittest 

24 

25import numpy as np 

26 

27import lsst.pex.exceptions 

28import lsst.daf.base as dafBase 

29import lsst.geom 

30import lsst.afw.detection as afwDetection 

31import lsst.afw.math as afwMath 

32import lsst.afw.geom as afwGeom 

33import lsst.afw.table as afwTable 

34import lsst.afw.image as afwImage 

35import lsst.meas.base as measBase 

36import lsst.utils.tests 

37 

38try: 

39 type(display) 

40except NameError: 

41 display = False 

42 

43FwhmPerSigma = 2*math.sqrt(2*math.log(2)) # FWHM for an N(0, 1) Gaussian 

44 

45 

46def makePluginAndCat(alg, name, control, metadata=False, centroid=None): 

47 schema = afwTable.SourceTable.makeMinimalSchema() 

48 if centroid: 

49 schema.addField(centroid + "_x", type=np.float64) 

50 schema.addField(centroid + "_y", type=np.float64) 

51 schema.addField(centroid + "_flag", type='Flag') 

52 schema.getAliasMap().set("slot_Centroid", centroid) 

53 if metadata: 

54 plugin = alg(control, name, schema, dafBase.PropertySet()) 

55 else: 

56 plugin = alg(control, name, schema) 

57 cat = afwTable.SourceCatalog(schema) 

58 return plugin, cat 

59 

60 

61class MeasureSourcesTestCase(lsst.utils.tests.TestCase): 

62 

63 def setUp(self): 

64 pass 

65 

66 def tearDown(self): 

67 pass 

68 

69 def testCircularApertureMeasure(self): 

70 mi = afwImage.MaskedImageF(lsst.geom.ExtentI(100, 200)) 

71 mi.set(10) 

72 # 

73 # Create our measuring engine 

74 # 

75 

76 radii = (1.0, 5.0, 10.0) # radii to use 

77 

78 control = measBase.ApertureFluxControl() 

79 control.radii = radii 

80 

81 exp = afwImage.makeExposure(mi) 

82 x0, y0 = 1234, 5678 

83 exp.setXY0(lsst.geom.Point2I(x0, y0)) 

84 

85 plugin, cat = makePluginAndCat(measBase.CircularApertureFluxAlgorithm, 

86 "test", control, True, centroid="centroid") 

87 source = cat.makeRecord() 

88 source.set("centroid_x", 30+x0) 

89 source.set("centroid_y", 50+y0) 

90 plugin.measure(source, exp) 

91 

92 for r in radii: 

93 currentFlux = source.get("%s_instFlux" % 

94 measBase.CircularApertureFluxAlgorithm.makeFieldPrefix("test", r)) 

95 self.assertAlmostEqual(10.0*math.pi*r*r/currentFlux, 1.0, places=4) 

96 

97 def testPeakLikelihoodFlux(self): 

98 """Test measurement with PeakLikelihoodFlux. 

99 

100 Notes 

101 ----- 

102 This test makes and measures a series of exposures containing just one 

103 star, approximately centered. 

104 """ 

105 

106 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(100, 101)) 

107 kernelWidth = 35 

108 var = 100 

109 fwhm = 3.0 

110 sigma = fwhm/FwhmPerSigma 

111 convolutionControl = afwMath.ConvolutionControl() 

112 psf = afwDetection.GaussianPsf(kernelWidth, kernelWidth, sigma) 

113 psfKernel = psf.getLocalKernel(psf.getAveragePosition()) 

114 psfImage = psf.computeKernelImage(psf.getAveragePosition()) 

115 sumPsfSq = np.sum(psfImage.getArray()**2) 

116 psfSqArr = psfImage.getArray()**2 

117 

118 for instFlux in (1000, 10000): 

119 ctrInd = lsst.geom.Point2I(50, 51) 

120 ctrPos = lsst.geom.Point2D(ctrInd) 

121 

122 kernelBBox = psfImage.getBBox() 

123 kernelBBox.shift(lsst.geom.Extent2I(ctrInd)) 

124 

125 # compute predicted instFlux error 

126 unshMImage = makeFakeImage(bbox, [ctrPos], [instFlux], fwhm, var) 

127 

128 # filter image by PSF 

129 unshFiltMImage = afwImage.MaskedImageF(unshMImage.getBBox()) 

130 afwMath.convolve(unshFiltMImage, unshMImage, psfKernel, convolutionControl) 

131 

132 # compute predicted instFlux = value of image at peak / sum(PSF^2) 

133 # this is a sanity check of the algorithm, as much as anything 

134 predFlux = unshFiltMImage.image[ctrInd, afwImage.LOCAL] / sumPsfSq 

135 self.assertLess(abs(instFlux - predFlux), instFlux * 0.01) 

136 

137 # compute predicted instFlux error based on filtered pixels 

138 # = sqrt(value of filtered variance at peak / sum(PSF^2)^2) 

139 predFluxErr = math.sqrt(unshFiltMImage.variance[ctrInd, afwImage.LOCAL]) / sumPsfSq 

140 

141 # compute predicted instFlux error based on unfiltered pixels 

142 # = sqrt(sum(unfiltered variance * PSF^2)) / sum(PSF^2) 

143 # and compare to that derived from filtered pixels; 

144 # again, this is a test of the algorithm 

145 varView = afwImage.ImageF(unshMImage.getVariance(), kernelBBox) 

146 varArr = varView.getArray() 

147 unfiltPredFluxErr = math.sqrt(np.sum(varArr*psfSqArr)) / sumPsfSq 

148 self.assertLess(abs(unfiltPredFluxErr - predFluxErr), predFluxErr * 0.01) 

149 

150 for fracOffset in (lsst.geom.Extent2D(0, 0), lsst.geom.Extent2D(0.2, -0.3)): 

151 adjCenter = ctrPos + fracOffset 

152 if fracOffset == lsst.geom.Extent2D(0, 0): 

153 maskedImage = unshMImage 

154 filteredImage = unshFiltMImage 

155 else: 

156 maskedImage = makeFakeImage(bbox, [adjCenter], [instFlux], fwhm, var) 

157 # filter image by PSF 

158 filteredImage = afwImage.MaskedImageF(maskedImage.getBBox()) 

159 afwMath.convolve(filteredImage, maskedImage, psfKernel, convolutionControl) 

160 

161 exp = afwImage.makeExposure(filteredImage) 

162 exp.setPsf(psf) 

163 control = measBase.PeakLikelihoodFluxControl() 

164 plugin, cat = makePluginAndCat(measBase.PeakLikelihoodFluxAlgorithm, "test", 

165 control, centroid="centroid") 

166 source = cat.makeRecord() 

167 source.set("centroid_x", adjCenter.getX()) 

168 source.set("centroid_y", adjCenter.getY()) 

169 plugin.measure(source, exp) 

170 measFlux = source.get("test_instFlux") 

171 measFluxErr = source.get("test_instFluxErr") 

172 self.assertLess(abs(measFlux - instFlux), instFlux * 0.003) 

173 

174 self.assertLess(abs(measFluxErr - predFluxErr), predFluxErr * 0.2) 

175 

176 # try nearby points and verify that the instFlux is smaller; 

177 # this checks that the sub-pixel shift is performed in the 

178 # correct direction 

179 for dx in (-0.2, 0, 0.2): 

180 for dy in (-0.2, 0, 0.2): 

181 if dx == dy == 0: 

182 continue 

183 offsetCtr = lsst.geom.Point2D(adjCenter[0] + dx, adjCenter[1] + dy) 

184 source = cat.makeRecord() 

185 source.set("centroid_x", offsetCtr.getX()) 

186 source.set("centroid_y", offsetCtr.getY()) 

187 plugin.measure(source, exp) 

188 self.assertLess(source.get("test_instFlux"), measFlux) 

189 

190 # source so near edge of image that PSF does not overlap exposure 

191 # should result in failure 

192 for edgePos in ( 

193 (1, 50), 

194 (50, 1), 

195 (50, bbox.getHeight() - 1), 

196 (bbox.getWidth() - 1, 50), 

197 ): 

198 source = cat.makeRecord() 

199 source.set("centroid_x", edgePos[0]) 

200 source.set("centroid_y", edgePos[1]) 

201 with self.assertRaises(lsst.pex.exceptions.RangeError): 

202 plugin.measure(source, exp) 

203 

204 # no PSF should result in failure: flags set 

205 noPsfExposure = afwImage.ExposureF(filteredImage) 

206 source = cat.makeRecord() 

207 source.set("centroid_x", edgePos[0]) 

208 source.set("centroid_y", edgePos[1]) 

209 with self.assertRaises(lsst.pex.exceptions.InvalidParameterError): 

210 plugin.measure(source, noPsfExposure) 

211 

212 def testPixelFlags(self): 

213 width, height = 100, 100 

214 mi = afwImage.MaskedImageF(width, height) 

215 exp = afwImage.makeExposure(mi) 

216 mi.getImage().set(0) 

217 mask = mi.getMask() 

218 sat = mask.getPlaneBitMask('SAT') 

219 interp = mask.getPlaneBitMask('INTRP') 

220 edge = mask.getPlaneBitMask('EDGE') 

221 bad = mask.getPlaneBitMask('BAD') 

222 nodata = mask.getPlaneBitMask('NO_DATA') 

223 mask.addMaskPlane('CLIPPED') 

224 clipped = mask.getPlaneBitMask('CLIPPED') 

225 mask.set(0) 

226 mask[20, 20, afwImage.LOCAL] = sat 

227 mask[60, 60, afwImage.LOCAL] = interp 

228 mask[40, 20, afwImage.LOCAL] = bad 

229 mask[20, 80, afwImage.LOCAL] = nodata 

230 mask[30, 30, afwImage.LOCAL] = clipped 

231 mask.Factory(mask, lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(3, height))).set(edge) 

232 x0, y0 = 1234, 5678 

233 exp.setXY0(lsst.geom.Point2I(x0, y0)) 

234 control = measBase.PixelFlagsControl() 

235 # Change the configuration of control to test for clipped mask 

236 control.masksFpAnywhere = ['CLIPPED'] 

237 plugin, cat = makePluginAndCat(measBase.PixelFlagsAlgorithm, "test", control, centroid="centroid") 

238 allFlags = [ 

239 "", 

240 "edge", 

241 "interpolated", 

242 "interpolatedCenter", 

243 "saturated", 

244 "saturatedCenter", 

245 "cr", 

246 "crCenter", 

247 "bad", 

248 "clipped", 

249 ] 

250 for x, y, setFlags in [(1, 50, ['edge']), 

251 (40, 20, ['bad']), 

252 (20, 20, ['saturatedCenter', 

253 'saturated']), 

254 (20, 22, ['saturated']), 

255 (60, 60, ['interpolatedCenter', 

256 'interpolated']), 

257 (60, 62, ['interpolated']), 

258 (20, 80, ['edge']), 

259 (30, 30, ['clipped']), 

260 ]: 

261 spans = afwGeom.SpanSet.fromShape(5).shiftedBy(x + x0, 

262 y + y0) 

263 foot = afwDetection.Footprint(spans) 

264 source = cat.makeRecord() 

265 source.setFootprint(foot) 

266 source.set("centroid_x", x+x0) 

267 source.set("centroid_y", y+y0) 

268 plugin.measure(source, exp) 

269 for flag in allFlags[1:]: 

270 value = source.get("test_flag_" + flag) 

271 if flag in setFlags: 

272 self.assertTrue(value, "Flag %s should be set for %f,%f" % (flag, x, y)) 

273 else: 

274 self.assertFalse(value, "Flag %s should not be set for %f,%f" % (flag, x, y)) 

275 

276 # the new code which grabs the center of a record throws when a NaN is 

277 # set in the centroid slot and the algorithm attempts to get the 

278 # default center position 

279 source = cat.makeRecord() 

280 source.set("centroid_x", float("NAN")) 

281 source.set("centroid_y", 40) 

282 source.set("centroid_flag", True) 

283 tmpSpanSet = afwGeom.SpanSet.fromShape(5).shiftedBy(x + x0, 

284 y + y0) 

285 source.setFootprint(afwDetection.Footprint(tmpSpanSet)) 

286 with self.assertRaises(lsst.pex.exceptions.RuntimeError): 

287 plugin.measure(source, exp) 

288 

289 # Test that if there is no center and centroider that the object 

290 # should look at the footprint 

291 plugin, cat = makePluginAndCat(measBase.PixelFlagsAlgorithm, "test", control) 

292 # The first test should raise exception because there is no footprint 

293 source = cat.makeRecord() 

294 with self.assertRaises(lsst.pex.exceptions.RuntimeError): 

295 plugin.measure(source, exp) 

296 # The second test will raise an error because no peaks are present 

297 tmpSpanSet2 = afwGeom.SpanSet.fromShape(5).shiftedBy(x + x0, 

298 y + y0) 

299 source.setFootprint(afwDetection.Footprint(tmpSpanSet2)) 

300 with self.assertRaises(lsst.pex.exceptions.RuntimeError): 

301 plugin.measure(source, exp) 

302 # The final test should pass because it detects a peak, we are reusing 

303 # the location of the clipped bit in the mask plane, so we will check 

304 # first that it is False, then True 

305 source.getFootprint().addPeak(x+x0, y+y0, 100) 

306 self.assertFalse(source.get("test_flag_clipped"), "The clipped flag should be set False") 

307 plugin.measure(source, exp) 

308 self.assertTrue(source.get("test_flag_clipped"), "The clipped flag should be set True") 

309 

310 

311def addStar(image, center, instFlux, fwhm): 

312 """Add a perfect single Gaussian star to an image. 

313 

314 Parameters 

315 ---------- 

316 image : `lsst.afw.image.ImageF` 

317 Image to which the star will be added. 

318 center : `list` or `tuple` of `float`, length 2 

319 Position of the center of the star on the image. 

320 instFlux : `float` 

321 instFlux of the Gaussian star, in counts. 

322 fwhm : `float` 

323 FWHM of the Gaussian star, in pixels. 

324 

325 Notes 

326 ----- 

327 Uses Python to iterate over all pixels (because there is no C++ 

328 function that computes a Gaussian offset by a non-integral amount). 

329 """ 

330 sigma = fwhm/FwhmPerSigma 

331 func = afwMath.GaussianFunction2D(sigma, sigma, 0) 

332 starImage = afwImage.ImageF(image.getBBox()) 

333 # The instFlux in the region of the image will not be exactly the desired 

334 # instFlux because the Gaussian does not extend to infinity, so keep track 

335 # of the actual instFlux and correct for it 

336 actFlux = 0 

337 # No function exists that has a fractional x and y offset, so set the 

338 # image the slow way 

339 for i in range(image.getWidth()): 

340 x = center[0] - i 

341 for j in range(image.getHeight()): 

342 y = center[1] - j 

343 pixVal = instFlux * func(x, y) 

344 actFlux += pixVal 

345 starImage[i, j, afwImage.LOCAL] += pixVal 

346 starImage *= instFlux / actFlux 

347 

348 image += starImage 

349 

350 

351def makeFakeImage(bbox, centerList, instFluxList, fwhm, var): 

352 """Make a fake image containing a set of stars with variance = image + var. 

353 

354 Paramters 

355 --------- 

356 bbox : `lsst.afw.image.Box2I` 

357 Bounding box for image. 

358 centerList : iterable of pairs of `float` 

359 list of positions of center of star on image. 

360 instFluxList : `list` of `float` 

361 instFlux of each star, in counts. 

362 fwhm : `float` 

363 FWHM of Gaussian star, in pixels. 

364 var : `float` 

365 Value of variance plane, in counts. 

366 

367 Returns 

368 ------- 

369 maskedImage : `lsst.afw.image.MaskedImageF` 

370 Resulting fake image. 

371 

372 Notes 

373 ----- 

374 It is trivial to add Poisson noise, which would be more accurate, but 

375 hard to make a unit test that can reliably determine whether such an 

376 image passes a test. 

377 """ 

378 if len(centerList) != len(instFluxList): 

379 raise RuntimeError("len(centerList) != len(instFluxList)") 

380 maskedImage = afwImage.MaskedImageF(bbox) 

381 image = maskedImage.getImage() 

382 for center, instFlux in zip(centerList, instFluxList): 

383 addStar(image, center=center, instFlux=instFlux, fwhm=fwhm) 

384 variance = maskedImage.getVariance() 

385 variance[:] = image 

386 variance += var 

387 return maskedImage 

388 

389 

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

391 pass 

392 

393 

394def setup_module(module): 

395 lsst.utils.tests.init() 

396 

397 

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

399 lsst.utils.tests.init() 

400 unittest.main()