Coverage for tests/test_psf.py: 16%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

154 statements  

1import unittest 

2import numpy as np 

3import copy 

4 

5import lsst.utils.tests 

6import lsst.afw.detection as afwDetection 

7import lsst.afw.geom as afwGeom 

8import lsst.afw.image as afwImage 

9import lsst.afw.math as afwMath 

10import lsst.afw.table as afwTable 

11import lsst.daf.base as dafBase 

12import lsst.geom as geom 

13import lsst.meas.algorithms as measAlg 

14from lsst.meas.base import SingleFrameMeasurementTask 

15import lsst.meas.extensions.piff.piffPsfDeterminer # noqa 

16 

17 

18def psfVal(ix, iy, x, y, sigma1, sigma2, b): 

19 """Return the value at (ix, iy) of a double Gaussian 

20 (N(0, sigma1^2) + b*N(0, sigma2^2))/(1 + b) 

21 centered at (x, y) 

22 """ 

23 dx, dy = x - ix, y - iy 

24 theta = np.radians(30) 

25 ab = 1.0/0.75 # axis ratio 

26 c, s = np.cos(theta), np.sin(theta) 

27 u, v = c*dx - s*dy, s*dx + c*dy 

28 

29 return (np.exp(-0.5*(u**2 + (v*ab)**2)/sigma1**2) 

30 + b*np.exp(-0.5*(u**2 + (v*ab)**2)/sigma2**2))/(1 + b) 

31 

32 

33class SpatialModelPsfTestCase(lsst.utils.tests.TestCase): 

34 """A test case for SpatialModelPsf""" 

35 

36 def measure(self, footprintSet, exposure): 

37 """Measure a set of Footprints, returning a SourceCatalog""" 

38 catalog = afwTable.SourceCatalog(self.schema) 

39 

40 footprintSet.makeSources(catalog) 

41 

42 self.measureSources.run(catalog, exposure) 

43 return catalog 

44 

45 def setUp(self): 

46 config = SingleFrameMeasurementTask.ConfigClass() 

47 config.slots.apFlux = 'base_CircularApertureFlux_12_0' 

48 self.schema = afwTable.SourceTable.makeMinimalSchema() 

49 

50 self.measureSources = SingleFrameMeasurementTask( 

51 self.schema, config=config 

52 ) 

53 self.usePsfFlag = self.schema.addField("use_psf", type="Flag") 

54 

55 width, height = 110, 301 

56 

57 self.mi = afwImage.MaskedImageF(geom.ExtentI(width, height)) 

58 self.mi.set(0) 

59 sd = 3 # standard deviation of image 

60 self.mi.getVariance().set(sd*sd) 

61 self.mi.getMask().addMaskPlane("DETECTED") 

62 

63 self.ksize = 31 # size of desired kernel 

64 

65 sigma1 = 1.75 

66 sigma2 = 2*sigma1 

67 

68 self.exposure = afwImage.makeExposure(self.mi) 

69 self.exposure.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize, 

70 1.5*sigma1, 1, 0.1)) 

71 cdMatrix = np.array([1.0, 0.0, 0.0, 1.0]) 

72 cdMatrix.shape = (2, 2) 

73 wcs = afwGeom.makeSkyWcs(crpix=geom.PointD(0, 0), 

74 crval=geom.SpherePoint(0.0, 0.0, geom.degrees), 

75 cdMatrix=cdMatrix) 

76 self.exposure.setWcs(wcs) 

77 

78 # 

79 # Make a kernel with the exactly correct basis functions. 

80 # Useful for debugging 

81 # 

82 basisKernelList = [] 

83 for sigma in (sigma1, sigma2): 

84 basisKernel = afwMath.AnalyticKernel( 

85 self.ksize, self.ksize, afwMath.GaussianFunction2D(sigma, sigma) 

86 ) 

87 basisImage = afwImage.ImageD(basisKernel.getDimensions()) 

88 basisKernel.computeImage(basisImage, True) 

89 basisImage /= np.sum(basisImage.getArray()) 

90 

91 if sigma == sigma1: 

92 basisImage0 = basisImage 

93 else: 

94 basisImage -= basisImage0 

95 

96 basisKernelList.append(afwMath.FixedKernel(basisImage)) 

97 

98 order = 1 # 1 => up to linear 

99 spFunc = afwMath.PolynomialFunction2D(order) 

100 

101 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

102 exactKernel.setSpatialParameters( 

103 [[1.0, 0, 0], 

104 [0.0, 0.5*1e-2, 0.2e-2]] 

105 ) 

106 

107 rand = afwMath.Random() # make these tests repeatable by setting seed 

108 

109 im = self.mi.getImage() 

110 afwMath.randomGaussianImage(im, rand) # N(0, 1) 

111 im *= sd # N(0, sd^2) 

112 

113 xarr, yarr = [], [] 

114 

115 for x, y in [(20, 20), (60, 20), 

116 (30, 35), 

117 (50, 50), 

118 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30), 

119 (50, 120), (70, 80), 

120 (60, 210), (20, 210), 

121 ]: 

122 xarr.append(x) 

123 yarr.append(y) 

124 

125 for x, y in zip(xarr, yarr): 

126 dx = rand.uniform() - 0.5 # random (centered) offsets 

127 dy = rand.uniform() - 0.5 

128 

129 k = exactKernel.getSpatialFunction(1)(x, y) 

130 b = (k*sigma1**2/((1 - k)*sigma2**2)) 

131 

132 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5)) 

133 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2)) 

134 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1): 

135 if iy < 0 or iy >= self.mi.getHeight(): 

136 continue 

137 

138 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1): 

139 if ix < 0 or ix >= self.mi.getWidth(): 

140 continue 

141 

142 II = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b) 

143 Isample = rand.poisson(II) 

144 self.mi.image[ix, iy, afwImage.LOCAL] += Isample 

145 self.mi.variance[ix, iy, afwImage.LOCAL] += II 

146 

147 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height)) 

148 self.cellSet = afwMath.SpatialCellSet(bbox, 100) 

149 

150 self.footprintSet = afwDetection.FootprintSet( 

151 self.mi, afwDetection.Threshold(100), "DETECTED" 

152 ) 

153 

154 self.catalog = self.measure(self.footprintSet, self.exposure) 

155 

156 for source in self.catalog: 

157 cand = measAlg.makePsfCandidate(source, self.exposure) 

158 self.cellSet.insertCandidate(cand) 

159 

160 def setupDeterminer(self, exposure): 

161 """Setup the starSelector and psfDeterminer""" 

162 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

163 starSelectorConfig = starSelectorClass.ConfigClass() 

164 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

165 starSelectorConfig.badFlags = [ 

166 "base_PixelFlags_flag_edge", 

167 "base_PixelFlags_flag_interpolatedCenter", 

168 "base_PixelFlags_flag_saturatedCenter", 

169 "base_PixelFlags_flag_crCenter", 

170 ] 

171 # Set to match when the tolerance of the test was set 

172 starSelectorConfig.widthStdAllowed = 0.5 

173 

174 self.starSelector = starSelectorClass(config=starSelectorConfig) 

175 

176 self.makePsfCandidates = measAlg.MakePsfCandidatesTask() 

177 

178 psfDeterminerClass = measAlg.psfDeterminerRegistry["piff"] 

179 psfDeterminerConfig = psfDeterminerClass.ConfigClass() 

180 width, height = exposure.getMaskedImage().getDimensions() 

181 psfDeterminerConfig.spatialOrder = 1 

182 

183 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig) 

184 

185 def subtractStars(self, exposure, catalog, chi_lim=-1): 

186 """Subtract the exposure's PSF from all the sources in catalog""" 

187 mi, psf = exposure.getMaskedImage(), exposure.getPsf() 

188 

189 subtracted = mi.Factory(mi, True) 

190 for s in catalog: 

191 xc, yc = s.getX(), s.getY() 

192 bbox = subtracted.getBBox(afwImage.PARENT) 

193 if bbox.contains(geom.PointI(int(xc), int(yc))): 

194 measAlg.subtractPsf(psf, subtracted, xc, yc) 

195 chi = subtracted.Factory(subtracted, True) 

196 var = subtracted.getVariance() 

197 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt 

198 chi /= var 

199 

200 chi_min = np.min(chi.getImage().getArray()) 

201 chi_max = np.max(chi.getImage().getArray()) 

202 print(chi_min, chi_max) 

203 

204 if chi_lim > 0: 

205 self.assertGreater(chi_min, -chi_lim) 

206 self.assertLess(chi_max, chi_lim) 

207 

208 def testPiffDeterminer(self): 

209 """Test the (Piff) psfDeterminer on subImages""" 

210 

211 self.setupDeterminer(self.exposure) 

212 metadata = dafBase.PropertyList() 

213 

214 stars = self.starSelector.run(self.catalog, exposure=self.exposure) 

215 psfCandidateList = self.makePsfCandidates.run( 

216 stars.sourceCat, 

217 exposure=self.exposure 

218 ).psfCandidates 

219 psf, cellSet = self.psfDeterminer.determinePsf( 

220 self.exposure, 

221 psfCandidateList, 

222 metadata, 

223 flagKey=self.usePsfFlag 

224 ) 

225 self.exposure.setPsf(psf) 

226 

227 self.assertEqual(len(psfCandidateList), metadata['numAvailStars']) 

228 self.assertEqual(sum(self.catalog['use_psf']), metadata['numGoodStars']) 

229 

230 # Test how well we can subtract the PSF model 

231 self.subtractStars(self.exposure, self.catalog, chi_lim=5.6) 

232 

233 # Test bboxes 

234 for point in [ 

235 psf.getAveragePosition(), 

236 geom.Point2D(), 

237 geom.Point2D(1, 1) 

238 ]: 

239 self.assertEqual( 

240 psf.computeBBox(point), 

241 psf.computeKernelImage(point).getBBox() 

242 ) 

243 self.assertEqual( 

244 psf.computeKernelBBox(point), 

245 psf.computeKernelImage(point).getBBox() 

246 ) 

247 self.assertEqual( 

248 psf.computeImageBBox(point), 

249 psf.computeImage(point).getBBox() 

250 ) 

251 

252 # Some roundtrips 

253 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

254 self.exposure.writeFits(tmpFile) 

255 fitsIm = afwImage.ExposureF(tmpFile) 

256 copyIm = copy.deepcopy(self.exposure) 

257 

258 for newIm in [fitsIm, copyIm]: 

259 # Piff doesn't enable __eq__ for its results, so we just check 

260 # that some PSF images come out the same. 

261 for point in [ 

262 geom.Point2D(0, 0), 

263 geom.Point2D(10, 100), 

264 geom.Point2D(-200, 30), 

265 geom.Point2D(float("nan")) # "nullPoint" 

266 ]: 

267 self.assertImagesAlmostEqual( 

268 psf.computeImage(point), 

269 newIm.getPsf().computeImage(point) 

270 ) 

271 # Also check using default position 

272 self.assertImagesAlmostEqual( 

273 psf.computeImage(), 

274 newIm.getPsf().computeImage() 

275 ) 

276 

277 

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

279 pass 

280 

281 

282def setup_module(module): 

283 lsst.utils.tests.init() 

284 

285 

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

287 lsst.utils.tests.init() 

288 unittest.main()