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

1# 

2# LSST Data Management System 

3# Copyright 2016-2017 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21import numpy as np 

22import unittest 

23 

24import lsst.afw.image as afwImage 

25import lsst.afw.math as afwMath 

26import lsst.afw.geom as afwGeom 

27import lsst.daf.base as dafBase 

28import lsst.geom as geom 

29from lsst.ip.diffim.zogy import ZogyTask, ZogyConfig 

30import lsst.meas.algorithms as measAlg 

31import lsst.utils.tests 

32from test_imageDecorrelation import singleGaussian2d 

33 

34try: 

35 type(verbose) 

36except NameError: 

37 verbose = False 

38 

39 

40def setup_module(module): 

41 lsst.utils.tests.init() 

42 

43 

44def makeFakeImages(size=(256, 256), svar=0.04, tvar=0.04, psf1=3.3, psf2=2.2, offset=None, 

45 psf_yvary_factor=0., varSourceChange=1/50., theta1=0., theta2=0., 

46 n_sources=50, seed=66, verbose=False): 

47 """Make two exposures: science and template pair with flux sources and random noise. 

48 In all cases below, index (1) is the science image, and (2) is the template. 

49 

50 Parameters 

51 ---------- 

52 size : `tuple` of `int` 

53 Image pixel size (x,y). Pixel coordinates are set to 

54 (-size[0]//2:size[0]//2, -size[1]//2:size[1]//2) 

55 svar, tvar : `float`, optional 

56 Per pixel variance of the added noise. 

57 psf1, psf2 : `float`, optional 

58 std. dev. of (Gaussian) PSFs for the two images in x,y direction. Default is 

59 [3.3, 3.3] and [2.2, 2.2] for im1 and im2 respectively. 

60 offset : `float`, optional 

61 add a constant (pixel) astrometric offset between the two images. 

62 psf_yvary_factor : `float`, optional 

63 psf_yvary_factor vary the y-width of the PSF across the x-axis of the science image (zero, 

64 the default, means no variation) 

65 varSourceChange : `float`, optional 

66 varSourceChange add this amount of fractional flux to a single source closest to 

67 the center of the science image. 

68 theta1, theta2: `float`, optional 

69 PSF Gaussian rotation angles in degrees. 

70 n_sources : `int`, optional 

71 The number of sources to add to the images. If zero, no sources are 

72 generated just background noise. 

73 seed : `int`, optional 

74 Random number generator seed. 

75 verbose : `bool`, optional 

76 Print some actual values. 

77 

78 Returns 

79 ------- 

80 im1, im2 : `lsst.afw.image.Exposure` 

81 The science and template exposures. 

82 

83 Notes 

84 ----- 

85 If ``n_sources > 0`` and ``varSourceChange > 0.`` exactly one source, 

86 that is closest to the center, will have different fluxes in the two 

87 generated images. The flux on the science image will be higher by 

88 ``varSourceChange`` fraction. 

89 

90 Having sources near the edges really messes up the 

91 fitting (probably because of the convolution). So we make sure no 

92 sources are near the edge. 

93 

94 Also it seems that having the variable source with a large 

95 flux increase also messes up the fitting (seems to lead to 

96 overfitting -- perhaps to the source itself). This might be fixed by 

97 adding more constant sources. 

98 """ 

99 rng = np.random.default_rng(seed) 

100 

101 psf1 = [3.3, 3.3] if psf1 is None else psf1 

102 if not hasattr(psf1, "__len__"): 

103 psf1 = [psf1, psf1] 

104 psf2 = [2.2, 2.2] if psf2 is None else psf2 

105 if not hasattr(psf2, "__len__"): 

106 psf2 = [psf2, psf2] 

107 offset = [0., 0.] if offset is None else offset # astrometric offset (pixels) between the two images 

108 if verbose: 

109 print('Science PSF:', psf1, theta1) 

110 print('Template PSF:', psf2, theta2) 

111 print(np.sqrt(psf1[0]**2 - psf2[0]**2)) 

112 print('Offset:', offset) 

113 

114 xim = np.arange(-size[0]//2, size[0]//2, 1) # Beware that -N//2 != -1*(N//2) for odd numbers 

115 yim = np.arange(-size[1]//2, size[1]//2, 1) 

116 x0im, y0im = np.meshgrid(xim, yim) 

117 

118 im1 = rng.normal(scale=np.sqrt(svar), size=x0im.shape) # variance of science image 

119 im2 = rng.normal(scale=np.sqrt(tvar), size=x0im.shape) # variance of template 

120 

121 if n_sources > 0: 

122 fluxes = rng.uniform(50, 30000, n_sources) 

123 xposns = rng.uniform(xim.min() + 16, xim.max() - 5, n_sources) 

124 yposns = rng.uniform(yim.min() + 16, yim.max() - 5, n_sources) 

125 

126 # Make the source closest to the center of the image the one that increases in flux 

127 ind = np.argmin(xposns**2. + yposns**2.) 

128 

129 # vary the y-width of psf across x-axis of science image (zero means no variation): 

130 psf1_yvary = psf_yvary_factor*(yim.mean() - yposns)/yim.max() 

131 if verbose: 

132 print('PSF y spatial-variation:', psf1_yvary.min(), psf1_yvary.max()) 

133 

134 for i in range(n_sources): 

135 flux = fluxes[i] 

136 tmp = flux*singleGaussian2d(x0im, y0im, xposns[i], yposns[i], psf2[0], psf2[1], theta=theta2) 

137 im2 += tmp 

138 if i == ind: 

139 flux += flux*varSourceChange 

140 tmp = flux*singleGaussian2d(x0im, y0im, xposns[i] + offset[0], yposns[i] + offset[1], 

141 psf1[0], psf1[1] + psf1_yvary[i], theta=theta1) 

142 im1 += tmp 

143 

144 im1_psf = singleGaussian2d(x0im, y0im, 0, 0, psf1[0], psf1[1], theta=theta1) 

145 im2_psf = singleGaussian2d(x0im, y0im, offset[0], offset[1], psf2[0], psf2[1], theta=theta2) 

146 

147 def makeWcs(offset=0): 

148 """ Make a fake Wcs 

149 

150 Parameters 

151 ---------- 

152 offset : float 

153 offset the Wcs by this many pixels. 

154 """ 

155 # taken from $AFW_DIR/tests/testMakeWcs.py 

156 metadata = dafBase.PropertySet() 

157 metadata.set("SIMPLE", "T") 

158 metadata.set("BITPIX", -32) 

159 metadata.set("NAXIS", 2) 

160 metadata.set("NAXIS1", 1024) 

161 metadata.set("NAXIS2", 1153) 

162 metadata.set("RADESYS", 'FK5') 

163 metadata.set("EQUINOX", 2000.) 

164 metadata.setDouble("CRVAL1", 215.604025685476) 

165 metadata.setDouble("CRVAL2", 53.1595451514076) 

166 metadata.setDouble("CRPIX1", 1109.99981456774 + offset) 

167 metadata.setDouble("CRPIX2", 560.018167811613 + offset) 

168 metadata.set("CTYPE1", 'RA---SIN') 

169 metadata.set("CTYPE2", 'DEC--SIN') 

170 metadata.setDouble("CD1_1", 5.10808596133527E-05) 

171 metadata.setDouble("CD1_2", 1.85579539217196E-07) 

172 metadata.setDouble("CD2_2", -5.10281493481982E-05) 

173 metadata.setDouble("CD2_1", -8.27440751733828E-07) 

174 return afwGeom.makeSkyWcs(metadata) 

175 

176 def makeExposure(imgArray, psfArray, imgVariance): 

177 """Convert an image and corresponding PSF into an exposure. 

178 

179 Set the (constant) variance plane equal to ``imgVariance``. 

180 

181 Parameters 

182 ---------- 

183 imgArray : `numpy.ndarray` 

184 2D array containing the image. 

185 psfArray : `numpy.ndarray` 

186 2D array containing the PSF image. 

187 imgVariance : `float` or `numpy.ndarray` 

188 Set the variance plane to this value. If an array, must be broadcastable to ``imgArray.shape``. 

189 

190 Returns 

191 ------- 

192 im1ex : `lsst.afw.image.Exposure` 

193 The new exposure. 

194 """ 

195 # All this code to convert the template image array/psf array into an exposure. 

196 bbox = geom.Box2I(geom.Point2I(0, 0), geom.Point2I(imgArray.shape[1] - 1, imgArray.shape[0] - 1)) 

197 im1ex = afwImage.ExposureD(bbox) 

198 im1ex.image.array[:, :] = imgArray 

199 im1ex.variance.array[:, :] = imgVariance 

200 psfBox = geom.Box2I(geom.Point2I(-12, -12), geom.Point2I(12, 12)) # a 25x25 pixel psf 

201 psf = afwImage.ImageD(psfBox) 

202 psfBox.shift(geom.Extent2I(-(-size[0]//2), -(-size[1]//2))) # -N//2 != -(N//2) for odd numbers 

203 im1_psf_sub = psfArray[psfBox.getMinY():psfBox.getMaxY() + 1, psfBox.getMinX():psfBox.getMaxX() + 1] 

204 psf.getArray()[:, :] = im1_psf_sub 

205 psfK = afwMath.FixedKernel(psf) 

206 psfNew = measAlg.KernelPsf(psfK) 

207 im1ex.setPsf(psfNew) 

208 wcs = makeWcs() 

209 im1ex.setWcs(wcs) 

210 return im1ex 

211 

212 im1ex = makeExposure(im1, im1_psf, svar) # Science image 

213 im2ex = makeExposure(im2, im2_psf, tvar) # Template 

214 

215 return im1ex, im2ex 

216 

217 

218class ZogyTest(lsst.utils.tests.TestCase): 

219 """A test case for the Zogy task. 

220 """ 

221 

222 def setUp(self): 

223 self.psf1_sigma = 3.3 # sigma of psf of science image 

224 self.psf2_sigma = 2.2 # sigma of psf of template image 

225 

226 self.statsControl = afwMath.StatisticsControl() 

227 self.statsControl.setNumSigmaClip(3.) 

228 self.statsControl.setNumIter(3) 

229 self.statsControl.setAndMask(afwImage.Mask 

230 .getPlaneBitMask(["INTRP", "EDGE", "SAT", "CR", 

231 "DETECTED", "BAD", 

232 "NO_DATA", "DETECTED_NEGATIVE"])) 

233 

234 def _computeVarianceMean(self, maskedIm): 

235 statObj = afwMath.makeStatistics(maskedIm.getVariance(), 

236 maskedIm.getMask(), afwMath.MEANCLIP, 

237 self.statsControl) 

238 mn = statObj.getValue(afwMath.MEANCLIP) 

239 return mn 

240 

241 def _computePixelVariance(self, maskedIm): 

242 statObj = afwMath.makeStatistics(maskedIm, afwMath.VARIANCECLIP, 

243 self.statsControl) 

244 var = statObj.getValue(afwMath.VARIANCECLIP) 

245 return var 

246 

247 def _computePixelMean(self, maskedIm): 

248 statObj = afwMath.makeStatistics(maskedIm, afwMath.MEANCLIP, 

249 self.statsControl) 

250 var = statObj.getValue(afwMath.MEANCLIP) 

251 return var 

252 

253 def testFourierTransformConvention(self): 

254 """Test numpy FFT normalization factor convention matches our assumption.""" 

255 D = np.arange(16).reshape(4, 4) 

256 fD = np.real(np.fft.fft2(D)) 

257 self.assertFloatsAlmostEqual( 

258 fD[0, 0], 120., rtol=None, 

259 msg="Numpy FFT does not use expected default normalization" 

260 " convention (1 in forward, 1/Npix in inverse operation).") 

261 

262 def testZogyNewImplementation(self): 

263 """DM-25115 implementation test. 

264 

265 Notes 

266 ----- 

267 See diffimTests: tickets/DM-25115_zogy_implementation/DM-25115_zogy_unit_test_development.ipynb 

268 """ 

269 

270 # self.svar = svar # variance of noise in science image 

271 # self.tvar = tvar # variance of noise in template image 

272 

273 # Sourceless case 

274 self.im1ex, self.im2ex \ 

275 = makeFakeImages(size=(256, 256), svar=100., tvar=100., 

276 psf1=self.psf1_sigma, psf2=self.psf2_sigma, 

277 n_sources=0, psf_yvary_factor=0, varSourceChange=0.1, 

278 seed=1, verbose=False) 

279 

280 config = ZogyConfig() 

281 config.scaleByCalibration = False 

282 task = ZogyTask(config=config) 

283 res = task.run(self.im1ex, self.im2ex) 

284 

285 bbox = res.diffExp.getBBox() 

286 subBbox = bbox.erodedBy(lsst.geom.Extent2I(25, 25)) 

287 subExp = res.diffExp[subBbox] 

288 pixvar = self._computePixelVariance(subExp.maskedImage) 

289 varmean = self._computeVarianceMean(subExp.maskedImage) 

290 # Due to 3 sigma clipping, this is not so precise 

291 self.assertFloatsAlmostEqual(pixvar, 200, rtol=0.1, atol=None) 

292 self.assertFloatsAlmostEqual(varmean, 200, rtol=0.05, atol=None) 

293 S = res.scoreExp.image.array / np.sqrt(res.scoreExp.variance.array) 

294 self.assertLess(np.amax(S), 5.) # Source not detected 

295 

296 # ========== 

297 self.im1ex, self.im2ex \ 

298 = makeFakeImages(size=(256, 256), svar=10., tvar=10., 

299 psf1=self.psf1_sigma, psf2=self.psf2_sigma, 

300 n_sources=10, psf_yvary_factor=0, varSourceChange=0.1, 

301 seed=1, verbose=False) 

302 task = ZogyTask(config=config) 

303 res = task.run(self.im1ex, self.im2ex) 

304 S = res.scoreExp.image.array / np.sqrt(res.scoreExp.variance.array) 

305 self.assertGreater(np.amax(S), 5.) # Source detected 

306 

307 

308class MemoryTester(lsst.utils.tests.MemoryTestCase): 

309 pass 

310 

311 

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

313 lsst.utils.tests.init() 

314 unittest.main()