Hide keyboard shortcuts

Hot-keys 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

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 self.assertEqual( 

230 psf.getAveragePosition(), 

231 geom.Point2D( 

232 np.mean([s.x for s in psf._piffResult.stars]), 

233 np.mean([s.y for s in psf._piffResult.stars]) 

234 ) 

235 ) 

236 

237 # Test how well we can subtract the PSF model 

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

239 

240 # Test bboxes 

241 for point in [ 

242 psf.getAveragePosition(), 

243 geom.Point2D(), 

244 geom.Point2D(1, 1) 

245 ]: 

246 self.assertEqual( 

247 psf.computeBBox(point), 

248 psf.computeKernelImage(point).getBBox() 

249 ) 

250 self.assertEqual( 

251 psf.computeKernelBBox(point), 

252 psf.computeKernelImage(point).getBBox() 

253 ) 

254 self.assertEqual( 

255 psf.computeImageBBox(point), 

256 psf.computeImage(point).getBBox() 

257 ) 

258 

259 # Some roundtrips 

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

261 self.exposure.writeFits(tmpFile) 

262 fitsIm = afwImage.ExposureF(tmpFile) 

263 copyIm = copy.deepcopy(self.exposure) 

264 

265 for newIm in [fitsIm, copyIm]: 

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

267 # that some PSF images come out the same. 

268 for point in [ 

269 geom.Point2D(0, 0), 

270 geom.Point2D(10, 100), 

271 geom.Point2D(-200, 30), 

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

273 ]: 

274 self.assertImagesAlmostEqual( 

275 psf.computeImage(point), 

276 newIm.getPsf().computeImage(point) 

277 ) 

278 # Also check average position 

279 newPsf = newIm.getPsf() 

280 self.assertImagesAlmostEqual( 

281 psf.computeImage(psf.getAveragePosition()), 

282 newPsf.computeImage(newPsf.getAveragePosition()) 

283 ) 

284 

285 

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

287 pass 

288 

289 

290def setup_module(module): 

291 lsst.utils.tests.init() 

292 

293 

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

295 lsst.utils.tests.init() 

296 unittest.main()