Coverage for tests/test_subtractTask.py: 9%

404 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-04 02:49 -0800

1# This file is part of ip_diffim. 

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 

23import numpy as np 

24 

25import lsst.afw.geom as afwGeom 

26from lsst.afw.image import PhotoCalib 

27import lsst.afw.image as afwImage 

28import lsst.afw.math as afwMath 

29import lsst.geom 

30from lsst.meas.algorithms.testUtils import plantSources 

31import lsst.ip.diffim.imagePsfMatch 

32from lsst.ip.diffim import subtractImages 

33from lsst.ip.diffim.utils import getPsfFwhm 

34from lsst.pex.config import FieldValidationError 

35import lsst.utils.tests 

36 

37 

38def makeFakeWcs(): 

39 """Make a fake, affine Wcs. 

40 """ 

41 crpix = lsst.geom.Point2D(123.45, 678.9) 

42 crval = lsst.geom.SpherePoint(0.1, 0.1, lsst.geom.degrees) 

43 cdMatrix = np.array([[5.19513851e-05, -2.81124812e-07], 

44 [-3.25186974e-07, -5.19112119e-05]]) 

45 return afwGeom.makeSkyWcs(crpix, crval, cdMatrix) 

46 

47 

48def makeTestImage(seed=5, nSrc=20, psfSize=2., noiseLevel=5., 

49 noiseSeed=6, fluxLevel=500., fluxRange=2., 

50 kernelSize=32, templateBorderSize=0, 

51 background=None, 

52 xSize=256, 

53 ySize=256, 

54 x0=12345, 

55 y0=67890, 

56 calibration=1., 

57 doApplyCalibration=False, 

58 ): 

59 """Make a reproduceable PSF-convolved exposure for testing. 

60 

61 Parameters 

62 ---------- 

63 seed : `int`, optional 

64 Seed value to initialize the random number generator for sources. 

65 nSrc : `int`, optional 

66 Number of sources to simulate. 

67 psfSize : `float`, optional 

68 Width of the PSF of the simulated sources, in pixels. 

69 noiseLevel : `float`, optional 

70 Standard deviation of the noise to add to each pixel. 

71 noiseSeed : `int`, optional 

72 Seed value to initialize the random number generator for noise. 

73 fluxLevel : `float`, optional 

74 Reference flux of the simulated sources. 

75 fluxRange : `float`, optional 

76 Range in flux amplitude of the simulated sources. 

77 kernelSize : `int`, optional 

78 Size in pixels of the kernel for simulating sources. 

79 templateBorderSize : `int`, optional 

80 Size in pixels of the image border used to pad the image. 

81 background : `lsst.afw.math.Chebyshev1Function2D`, optional 

82 Optional background to add to the output image. 

83 xSize, ySize : `int`, optional 

84 Size in pixels of the simulated image. 

85 x0, y0 : `int`, optional 

86 Origin of the image. 

87 calibration : `float`, optional 

88 Conversion factor between instFlux and nJy. 

89 doApplyCalibration : `bool`, optional 

90 Apply the photometric calibration and return the image in nJy? 

91 

92 Returns 

93 ------- 

94 modelExposure : `lsst.afw.image.Exposure` 

95 The model image, with the mask and variance planes. 

96 sourceCat : `lsst.afw.table.SourceCatalog` 

97 Catalog of sources detected on the model image. 

98 """ 

99 # Distance from the inner edge of the bounding box to avoid placing test 

100 # sources in the model images. 

101 bufferSize = kernelSize/2 + templateBorderSize + 1 

102 

103 bbox = lsst.geom.Box2I(lsst.geom.Point2I(x0, y0), lsst.geom.Extent2I(xSize, ySize)) 

104 if templateBorderSize > 0: 

105 bbox.grow(templateBorderSize) 

106 

107 rng = np.random.RandomState(seed) 

108 rngNoise = np.random.RandomState(noiseSeed) 

109 x0, y0 = bbox.getBegin() 

110 xSize, ySize = bbox.getDimensions() 

111 xLoc = rng.rand(nSrc)*(xSize - 2*bufferSize) + bufferSize + x0 

112 yLoc = rng.rand(nSrc)*(ySize - 2*bufferSize) + bufferSize + y0 

113 

114 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*fluxLevel 

115 sigmas = [psfSize for src in range(nSrc)] 

116 coordList = list(zip(xLoc, yLoc, flux, sigmas)) 

117 skyLevel = 0 

118 # Don't use the built in poisson noise: it modifies the global state of numpy random 

119 modelExposure = plantSources(bbox, kernelSize, skyLevel, coordList, addPoissonNoise=False) 

120 modelExposure.setWcs(makeFakeWcs()) 

121 noise = rngNoise.randn(ySize, xSize)*noiseLevel 

122 noise -= np.mean(noise) 

123 modelExposure.variance.array = np.sqrt(np.abs(modelExposure.image.array)) + noiseLevel**2 

124 modelExposure.image.array += noise 

125 

126 # Run source detection to set up the mask plane 

127 psfMatchTask = lsst.ip.diffim.imagePsfMatch.ImagePsfMatchTask() 

128 sourceCat = psfMatchTask.getSelectSources(modelExposure) 

129 modelExposure.setPhotoCalib(PhotoCalib(calibration, 0., bbox)) 

130 if background is not None: 

131 modelExposure.image += background 

132 modelExposure.maskedImage /= calibration 

133 if doApplyCalibration: 

134 modelExposure.maskedImage = modelExposure.photoCalib.calibrateImage(modelExposure.maskedImage) 

135 

136 return modelExposure, sourceCat 

137 

138 

139class AlardLuptonSubtractTest(lsst.utils.tests.TestCase): 

140 

141 def test_allowed_config_modes(self): 

142 """Verify the allowable modes for convolution. 

143 """ 

144 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

145 config.mode = 'auto' 

146 config.mode = 'convolveScience' 

147 config.mode = 'convolveTemplate' 

148 

149 with self.assertRaises(FieldValidationError): 

150 config.mode = 'aotu' 

151 

152 def test_mismatched_template(self): 

153 """Test that an error is raised if the template 

154 does not fully contain the science image. 

155 """ 

156 xSize = 200 

157 ySize = 200 

158 science, sources = makeTestImage(psfSize=2.4, xSize=xSize + 20, ySize=ySize + 20) 

159 template, _ = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize, doApplyCalibration=True) 

160 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

161 task = subtractImages.AlardLuptonSubtractTask(config=config) 

162 with self.assertRaises(AssertionError): 

163 task.run(template, science, sources) 

164 

165 def test_equal_images(self): 

166 """Test that running with enough sources produces reasonable output, 

167 with the same size psf in the template and science. 

168 """ 

169 noiseLevel = 1. 

170 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6) 

171 template, _ = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=7, 

172 templateBorderSize=20, doApplyCalibration=True) 

173 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

174 config.doSubtractBackground = False 

175 task = subtractImages.AlardLuptonSubtractTask(config=config) 

176 output = task.run(template, science, sources) 

177 # There shoud be no NaN values in the difference image 

178 self.assertTrue(np.all(np.isfinite(output.difference.image.array))) 

179 # Mean of difference image should be close to zero. 

180 meanError = noiseLevel/np.sqrt(output.difference.image.array.size) 

181 # Make sure to include pixels with the DETECTED mask bit set. 

182 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

183 differenceMean = _computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl) 

184 self.assertFloatsAlmostEqual(differenceMean, 0, atol=5*meanError) 

185 # stddev of difference image should be close to expected value. 

186 differenceStd = _computeRobustStatistics(output.difference.image, output.difference.mask, 

187 _makeStats(), statistic=afwMath.STDEV) 

188 self.assertFloatsAlmostEqual(differenceStd, np.sqrt(2)*noiseLevel, rtol=0.1) 

189 

190 def test_auto_convolveTemplate(self): 

191 """Test that auto mode gives the same result as convolveTemplate when 

192 the template psf is the smaller. 

193 """ 

194 noiseLevel = 1. 

195 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6) 

196 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, 

197 templateBorderSize=20, doApplyCalibration=True) 

198 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

199 config.doSubtractBackground = False 

200 config.mode = "convolveTemplate" 

201 

202 task = subtractImages.AlardLuptonSubtractTask(config=config) 

203 output = task.run(template.clone(), science.clone(), sources) 

204 

205 config.mode = "auto" 

206 task = subtractImages.AlardLuptonSubtractTask(config=config) 

207 outputAuto = task.run(template, science, sources) 

208 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage) 

209 

210 def test_auto_convolveScience(self): 

211 """Test that auto mode gives the same result as convolveScience when 

212 the science psf is the smaller. 

213 """ 

214 noiseLevel = 1. 

215 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6) 

216 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7, 

217 templateBorderSize=20, doApplyCalibration=True) 

218 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

219 config.doSubtractBackground = False 

220 config.mode = "convolveScience" 

221 

222 task = subtractImages.AlardLuptonSubtractTask(config=config) 

223 output = task.run(template.clone(), science.clone(), sources) 

224 

225 config.mode = "auto" 

226 task = subtractImages.AlardLuptonSubtractTask(config=config) 

227 outputAuto = task.run(template, science, sources) 

228 self.assertMaskedImagesEqual(output.difference.maskedImage, outputAuto.difference.maskedImage) 

229 

230 def test_science_better(self): 

231 """Test that running with enough sources produces reasonable output, 

232 with the science psf being smaller than the template. 

233 """ 

234 statsCtrl = _makeStats() 

235 statsCtrlDetect = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

236 

237 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel): 

238 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6) 

239 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7, 

240 templateBorderSize=20, doApplyCalibration=True) 

241 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

242 config.doSubtractBackground = False 

243 config.mode = "convolveScience" 

244 task = subtractImages.AlardLuptonSubtractTask(config=config) 

245 output = task.run(template, science, sources) 

246 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05) 

247 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05) 

248 # Mean of difference image should be close to zero. 

249 nGoodPix = np.sum(np.isfinite(output.difference.image.array)) 

250 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(nGoodPix) 

251 diffimMean = _computeRobustStatistics(output.difference.image, output.difference.mask, 

252 statsCtrlDetect) 

253 

254 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError) 

255 # stddev of difference image should be close to expected value. 

256 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2) 

257 varianceMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, 

258 statsCtrl) 

259 diffimStd = _computeRobustStatistics(output.difference.image, output.difference.mask, 

260 statsCtrl, statistic=afwMath.STDEV) 

261 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1) 

262 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1) 

263 

264 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.) 

265 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1) 

266 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1) 

267 

268 def test_template_better(self): 

269 """Test that running with enough sources produces reasonable output, 

270 with the template psf being smaller than the science. 

271 """ 

272 statsCtrl = _makeStats() 

273 statsCtrlDetect = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

274 

275 def _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel, templateNoiseLevel): 

276 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6) 

277 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7, 

278 templateBorderSize=20, doApplyCalibration=True) 

279 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

280 config.doSubtractBackground = False 

281 task = subtractImages.AlardLuptonSubtractTask(config=config) 

282 output = task.run(template, science, sources) 

283 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 1., atol=.05) 

284 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 1., atol=.05) 

285 # There should be no NaNs in the image if we convolve the template with a buffer 

286 self.assertTrue(np.all(np.isfinite(output.difference.image.array))) 

287 # Mean of difference image should be close to zero. 

288 meanError = (scienceNoiseLevel + templateNoiseLevel)/np.sqrt(output.difference.image.array.size) 

289 

290 diffimMean = _computeRobustStatistics(output.difference.image, output.difference.mask, 

291 statsCtrlDetect) 

292 self.assertFloatsAlmostEqual(diffimMean, 0, atol=5*meanError) 

293 # stddev of difference image should be close to expected value. 

294 noiseLevel = np.sqrt(scienceNoiseLevel**2 + templateNoiseLevel**2) 

295 varianceMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, 

296 statsCtrl) 

297 diffimStd = _computeRobustStatistics(output.difference.image, output.difference.mask, 

298 statsCtrl, statistic=afwMath.STDEV) 

299 self.assertFloatsAlmostEqual(varianceMean, noiseLevel**2, rtol=0.1) 

300 self.assertFloatsAlmostEqual(diffimStd, noiseLevel, rtol=0.1) 

301 

302 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=1.) 

303 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=1., templateNoiseLevel=.1) 

304 _run_and_check_images(statsCtrl, statsCtrlDetect, scienceNoiseLevel=.1, templateNoiseLevel=.1) 

305 

306 def test_symmetry(self): 

307 """Test that convolving the science and convolving the template are 

308 symmetric: if the psfs are switched between them, the difference image 

309 should be nearly the same. 

310 """ 

311 noiseLevel = 1. 

312 # Don't include a border for the template, in order to make the results 

313 # comparable when we swap which image is treated as the "science" image. 

314 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, 

315 noiseSeed=6, templateBorderSize=0) 

316 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, 

317 noiseSeed=7, templateBorderSize=0, doApplyCalibration=True) 

318 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

319 config.mode = 'auto' 

320 config.doSubtractBackground = False 

321 task = subtractImages.AlardLuptonSubtractTask(config=config) 

322 

323 # The science image will be modified in place, so use a copy for the second run. 

324 science_better = task.run(template.clone(), science.clone(), sources) 

325 template_better = task.run(science, template, sources) 

326 

327 delta = template_better.difference.clone() 

328 delta.image -= science_better.difference.image 

329 delta.variance -= science_better.difference.variance 

330 delta.mask.array -= science_better.difference.mask.array 

331 

332 statsCtrl = _makeStats() 

333 # Mean of delta should be very close to zero. 

334 nGoodPix = np.sum(np.isfinite(delta.image.array)) 

335 meanError = 2*noiseLevel/np.sqrt(nGoodPix) 

336 deltaMean = _computeRobustStatistics(delta.image, delta.mask, statsCtrl) 

337 deltaStd = _computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV) 

338 self.assertFloatsAlmostEqual(deltaMean, 0, atol=5*meanError) 

339 # stddev of difference image should be close to expected value 

340 self.assertFloatsAlmostEqual(deltaStd, 2*np.sqrt(2)*noiseLevel, rtol=.1) 

341 

342 def test_few_sources(self): 

343 """Test with only 1 source, to check that we get a useful error. 

344 """ 

345 xSize = 256 

346 ySize = 256 

347 science, sources = makeTestImage(psfSize=2.4, nSrc=10, xSize=xSize, ySize=ySize) 

348 template, _ = makeTestImage(psfSize=2.0, nSrc=10, xSize=xSize, ySize=ySize, doApplyCalibration=True) 

349 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

350 task = subtractImages.AlardLuptonSubtractTask(config=config) 

351 sources = sources[0:1] 

352 with self.assertRaisesRegex(RuntimeError, 

353 "Cannot compute PSF matching kernel: too few sources selected."): 

354 task.run(template, science, sources) 

355 

356 def test_order_equal_images(self): 

357 """Verify that the result is the same regardless of convolution mode 

358 if the images are equivalent. 

359 """ 

360 noiseLevel = .1 

361 seed1 = 6 

362 seed2 = 7 

363 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1) 

364 template1, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2, 

365 templateBorderSize=0, doApplyCalibration=True) 

366 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

367 config1.mode = "convolveTemplate" 

368 config1.doSubtractBackground = False 

369 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

370 results_convolveTemplate = task1.run(template1, science1, sources1) 

371 

372 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1) 

373 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed2, 

374 templateBorderSize=0, doApplyCalibration=True) 

375 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

376 config2.mode = "convolveScience" 

377 config2.doSubtractBackground = False 

378 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

379 results_convolveScience = task2.run(template2, science2, sources2) 

380 diff1 = science1.maskedImage.clone() 

381 diff1 -= template1.maskedImage 

382 diff2 = science2.maskedImage.clone() 

383 diff2 -= template2.maskedImage 

384 self.assertFloatsAlmostEqual(results_convolveTemplate.difference.image.array, 

385 diff1.image.array, 

386 atol=noiseLevel*5.) 

387 self.assertFloatsAlmostEqual(results_convolveScience.difference.image.array, 

388 diff2.image.array, 

389 atol=noiseLevel*5.) 

390 diffErr = noiseLevel*2 

391 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage, 

392 results_convolveScience.difference.maskedImage, 

393 atol=diffErr*5.) 

394 

395 def test_background_subtraction(self): 

396 """Check that we can recover the background, 

397 and that it is subtracted correctly in the difference image. 

398 """ 

399 noiseLevel = 1. 

400 xSize = 512 

401 ySize = 512 

402 x0 = 123 

403 y0 = 456 

404 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, 

405 templateBorderSize=20, 

406 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

407 doApplyCalibration=True) 

408 params = [2.2, 2.1, 2.0, 1.2, 1.1, 1.0] 

409 

410 bbox2D = lsst.geom.Box2D(lsst.geom.Point2D(x0, y0), lsst.geom.Extent2D(xSize, ySize)) 

411 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

412 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6, 

413 background=background_model, 

414 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

415 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

416 config.doSubtractBackground = True 

417 

418 config.makeKernel.kernel.name = "AL" 

419 config.makeKernel.kernel.active.fitForBackground = True 

420 config.makeKernel.kernel.active.spatialKernelOrder = 1 

421 config.makeKernel.kernel.active.spatialBgOrder = 2 

422 statsCtrl = _makeStats() 

423 

424 def _run_and_check_images(config, statsCtrl, mode): 

425 """Check that the fit background matches the input model. 

426 """ 

427 config.mode = mode 

428 task = subtractImages.AlardLuptonSubtractTask(config=config) 

429 output = task.run(template.clone(), science.clone(), sources) 

430 

431 # We should be fitting the same number of parameters as were in the input model 

432 self.assertEqual(output.backgroundModel.getNParameters(), background_model.getNParameters()) 

433 

434 # The parameters of the background fit should be close to the input model 

435 self.assertFloatsAlmostEqual(np.array(output.backgroundModel.getParameters()), 

436 np.array(params), rtol=0.3) 

437 

438 # stddev of difference image should be close to expected value. 

439 # This will fail if we have mis-subtracted the background. 

440 stdVal = _computeRobustStatistics(output.difference.image, output.difference.mask, 

441 statsCtrl, statistic=afwMath.STDEV) 

442 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel, rtol=0.1) 

443 

444 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

445 _run_and_check_images(config, statsCtrl, "convolveScience") 

446 

447 def test_scale_variance_convolve_template(self): 

448 """Check variance scaling of the image difference. 

449 """ 

450 scienceNoiseLevel = 4. 

451 templateNoiseLevel = 2. 

452 scaleFactor = 1.345 

453 # Make sure to include pixels with the DETECTED mask bit set. 

454 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

455 

456 def _run_and_check_images(science, template, sources, statsCtrl, 

457 doDecorrelation, doScaleVariance, scaleFactor=1.): 

458 """Check that the variance plane matches the expected value for 

459 different configurations of ``doDecorrelation`` and ``doScaleVariance``. 

460 """ 

461 

462 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

463 config.doSubtractBackground = False 

464 config.doDecorrelation = doDecorrelation 

465 config.doScaleVariance = doScaleVariance 

466 task = subtractImages.AlardLuptonSubtractTask(config=config) 

467 output = task.run(template.clone(), science.clone(), sources) 

468 if doScaleVariance: 

469 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 

470 scaleFactor, atol=0.05) 

471 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 

472 scaleFactor, atol=0.05) 

473 

474 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl) 

475 if doDecorrelation: 

476 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl) 

477 else: 

478 templateNoise = _computeRobustStatistics(output.matchedTemplate.variance, 

479 output.matchedTemplate.mask, 

480 statsCtrl) 

481 

482 if doScaleVariance: 

483 templateNoise *= scaleFactor 

484 scienceNoise *= scaleFactor 

485 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl) 

486 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1) 

487 

488 science, sources = makeTestImage(psfSize=3.0, noiseLevel=scienceNoiseLevel, noiseSeed=6) 

489 template, _ = makeTestImage(psfSize=2.0, noiseLevel=templateNoiseLevel, noiseSeed=7, 

490 templateBorderSize=20, doApplyCalibration=True) 

491 # Verify that the variance plane of the difference image is correct 

492 # when the template and science variance planes are correct 

493 _run_and_check_images(science, template, sources, statsCtrl, 

494 doDecorrelation=True, doScaleVariance=True) 

495 _run_and_check_images(science, template, sources, statsCtrl, 

496 doDecorrelation=True, doScaleVariance=False) 

497 _run_and_check_images(science, template, sources, statsCtrl, 

498 doDecorrelation=False, doScaleVariance=True) 

499 _run_and_check_images(science, template, sources, statsCtrl, 

500 doDecorrelation=False, doScaleVariance=False) 

501 

502 # Verify that the variance plane of the difference image is correct 

503 # when the template variance plane is incorrect 

504 template.variance.array /= scaleFactor 

505 science.variance.array /= scaleFactor 

506 _run_and_check_images(science, template, sources, statsCtrl, 

507 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

508 _run_and_check_images(science, template, sources, statsCtrl, 

509 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor) 

510 _run_and_check_images(science, template, sources, statsCtrl, 

511 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor) 

512 _run_and_check_images(science, template, sources, statsCtrl, 

513 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

514 

515 def test_scale_variance_convolve_science(self): 

516 """Check variance scaling of the image difference. 

517 """ 

518 scienceNoiseLevel = 4. 

519 templateNoiseLevel = 2. 

520 scaleFactor = 1.345 

521 # Make sure to include pixels with the DETECTED mask bit set. 

522 statsCtrl = _makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

523 

524 def _run_and_check_images(science, template, sources, statsCtrl, 

525 doDecorrelation, doScaleVariance, scaleFactor=1.): 

526 """Check that the variance plane matches the expected value for 

527 different configurations of ``doDecorrelation`` and ``doScaleVariance``. 

528 """ 

529 

530 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

531 config.mode = "convolveScience" 

532 config.doSubtractBackground = False 

533 config.doDecorrelation = doDecorrelation 

534 config.doScaleVariance = doScaleVariance 

535 task = subtractImages.AlardLuptonSubtractTask(config=config) 

536 output = task.run(template.clone(), science.clone(), sources) 

537 if doScaleVariance: 

538 self.assertFloatsAlmostEqual(task.metadata["scaleTemplateVarianceFactor"], 

539 scaleFactor, atol=0.05) 

540 self.assertFloatsAlmostEqual(task.metadata["scaleScienceVarianceFactor"], 

541 scaleFactor, atol=0.05) 

542 

543 templateNoise = _computeRobustStatistics(template.variance, template.mask, statsCtrl) 

544 if doDecorrelation: 

545 scienceNoise = _computeRobustStatistics(science.variance, science.mask, statsCtrl) 

546 else: 

547 scienceNoise = _computeRobustStatistics(output.matchedScience.variance, 

548 output.matchedScience.mask, 

549 statsCtrl) 

550 

551 if doScaleVariance: 

552 templateNoise *= scaleFactor 

553 scienceNoise *= scaleFactor 

554 

555 varMean = _computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl) 

556 self.assertFloatsAlmostEqual(varMean, scienceNoise + templateNoise, rtol=0.1) 

557 

558 science, sources = makeTestImage(psfSize=2.0, noiseLevel=scienceNoiseLevel, noiseSeed=6) 

559 template, _ = makeTestImage(psfSize=3.0, noiseLevel=templateNoiseLevel, noiseSeed=7, 

560 templateBorderSize=20, doApplyCalibration=True) 

561 # Verify that the variance plane of the difference image is correct 

562 # when the template and science variance planes are correct 

563 _run_and_check_images(science, template, sources, statsCtrl, 

564 doDecorrelation=True, doScaleVariance=True) 

565 _run_and_check_images(science, template, sources, statsCtrl, 

566 doDecorrelation=True, doScaleVariance=False) 

567 _run_and_check_images(science, template, sources, statsCtrl, 

568 doDecorrelation=False, doScaleVariance=True) 

569 _run_and_check_images(science, template, sources, statsCtrl, 

570 doDecorrelation=False, doScaleVariance=False) 

571 

572 # Verify that the variance plane of the difference image is correct 

573 # when the template and science variance planes are incorrect 

574 science.variance.array /= scaleFactor 

575 template.variance.array /= scaleFactor 

576 _run_and_check_images(science, template, sources, statsCtrl, 

577 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

578 _run_and_check_images(science, template, sources, statsCtrl, 

579 doDecorrelation=True, doScaleVariance=False, scaleFactor=scaleFactor) 

580 _run_and_check_images(science, template, sources, statsCtrl, 

581 doDecorrelation=False, doScaleVariance=True, scaleFactor=scaleFactor) 

582 _run_and_check_images(science, template, sources, statsCtrl, 

583 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

584 

585 def test_exposure_properties_convolve_template(self): 

586 """Check that all necessary exposure metadata is included 

587 when the template is convolved. 

588 """ 

589 noiseLevel = 1. 

590 seed = 37 

591 rng = np.random.RandomState(seed) 

592 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6) 

593 psf = science.psf 

594 psfAvgPos = psf.getAveragePosition() 

595 psfSize = getPsfFwhm(science.psf) 

596 psfImg = psf.computeKernelImage(psfAvgPos) 

597 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, 

598 templateBorderSize=20, doApplyCalibration=True) 

599 

600 # Generate a random aperture correction map 

601 apCorrMap = lsst.afw.image.ApCorrMap() 

602 for name in ("a", "b", "c"): 

603 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3))) 

604 science.info.setApCorrMap(apCorrMap) 

605 

606 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

607 config.mode = "convolveTemplate" 

608 

609 def _run_and_check_images(doDecorrelation): 

610 """Check that the metadata is correct with or without decorrelation. 

611 """ 

612 config.doDecorrelation = doDecorrelation 

613 task = subtractImages.AlardLuptonSubtractTask(config=config) 

614 output = task.run(template.clone(), science.clone(), sources) 

615 psfOut = output.difference.psf 

616 psfAvgPos = psfOut.getAveragePosition() 

617 if doDecorrelation: 

618 # Decorrelation requires recalculating the PSF, 

619 # so it will not be the same as the input 

620 psfOutSize = getPsfFwhm(science.psf) 

621 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

622 else: 

623 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

624 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

625 

626 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction 

627 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap()) 

628 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox()) 

629 self.assertEqual(science.filter, output.difference.filter) 

630 self.assertEqual(science.photoCalib, output.difference.photoCalib) 

631 _run_and_check_images(doDecorrelation=True) 

632 _run_and_check_images(doDecorrelation=False) 

633 

634 def test_exposure_properties_convolve_science(self): 

635 """Check that all necessary exposure metadata is included 

636 when the science image is convolved. 

637 """ 

638 noiseLevel = 1. 

639 seed = 37 

640 rng = np.random.RandomState(seed) 

641 science, sources = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=6) 

642 template, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=7, 

643 templateBorderSize=20, doApplyCalibration=True) 

644 psf = template.psf 

645 psfAvgPos = psf.getAveragePosition() 

646 psfSize = getPsfFwhm(template.psf) 

647 psfImg = psf.computeKernelImage(psfAvgPos) 

648 

649 # Generate a random aperture correction map 

650 apCorrMap = lsst.afw.image.ApCorrMap() 

651 for name in ("a", "b", "c"): 

652 apCorrMap.set(name, lsst.afw.math.ChebyshevBoundedField(science.getBBox(), rng.randn(3, 3))) 

653 science.info.setApCorrMap(apCorrMap) 

654 

655 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

656 config.mode = "convolveScience" 

657 

658 def _run_and_check_images(doDecorrelation): 

659 """Check that the metadata is correct with or without decorrelation. 

660 """ 

661 config.doDecorrelation = doDecorrelation 

662 task = subtractImages.AlardLuptonSubtractTask(config=config) 

663 output = task.run(template.clone(), science.clone(), sources) 

664 if doDecorrelation: 

665 # Decorrelation requires recalculating the PSF, 

666 # so it will not be the same as the input 

667 psfOutSize = getPsfFwhm(template.psf) 

668 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

669 else: 

670 psfOut = output.difference.psf 

671 psfAvgPos = psfOut.getAveragePosition() 

672 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

673 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

674 

675 # check PSF, WCS, bbox, filterLabel, photoCalib, aperture correction 

676 self._compare_apCorrMaps(apCorrMap, output.difference.info.getApCorrMap()) 

677 self.assertWcsAlmostEqualOverBBox(science.wcs, output.difference.wcs, science.getBBox()) 

678 self.assertEqual(science.filter, output.difference.filter) 

679 self.assertEqual(science.photoCalib, output.difference.photoCalib) 

680 

681 _run_and_check_images(doDecorrelation=True) 

682 _run_and_check_images(doDecorrelation=False) 

683 

684 def _compare_apCorrMaps(self, a, b): 

685 """Compare two ApCorrMaps for equality, without assuming that their BoundedFields have the 

686 same addresses (i.e. so we can compare after serialization). 

687 

688 This function is taken from ``ApCorrMapTestCase`` in afw/tests/. 

689 

690 Parameters 

691 ---------- 

692 a, b : `lsst.afw.image.ApCorrMap` 

693 The two aperture correction maps to compare. 

694 """ 

695 self.assertEqual(len(a), len(b)) 

696 for name, value in list(a.items()): 

697 value2 = b.get(name) 

698 self.assertIsNotNone(value2) 

699 self.assertEqual(value.getBBox(), value2.getBBox()) 

700 self.assertFloatsAlmostEqual( 

701 value.getCoefficients(), value2.getCoefficients(), rtol=0.0) 

702 

703 

704def _makeStats(badMaskPlanes=None): 

705 """Create a statistics control for configuring calculations on images. 

706 

707 Parameters 

708 ---------- 

709 badMaskPlanes : `list` of `str`, optional 

710 List of mask planes to exclude from calculations. 

711 

712 Returns 

713 ------- 

714 statsControl : ` lsst.afw.math.StatisticsControl` 

715 Statistics control object for configuring calculations on images. 

716 """ 

717 if badMaskPlanes is None: 

718 badMaskPlanes = ("INTRP", "EDGE", "DETECTED", "SAT", "CR", 

719 "BAD", "NO_DATA", "DETECTED_NEGATIVE") 

720 statsControl = afwMath.StatisticsControl() 

721 statsControl.setNumSigmaClip(3.) 

722 statsControl.setNumIter(3) 

723 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(badMaskPlanes)) 

724 return statsControl 

725 

726 

727def _computeRobustStatistics(image, mask, statsCtrl, statistic=afwMath.MEANCLIP): 

728 """Calculate a robust mean of the variance plane of an exposure. 

729 

730 Parameters 

731 ---------- 

732 image : `lsst.afw.image.Image` 

733 Image or variance plane of an exposure to evaluate. 

734 mask : `lsst.afw.image.Mask` 

735 Mask plane to use for excluding pixels. 

736 statsCtrl : `lsst.afw.math.StatisticsControl` 

737 Statistics control object for configuring the calculation. 

738 statistic : `lsst.afw.math.Property`, optional 

739 The type of statistic to compute. Typical values are 

740 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``. 

741 

742 Returns 

743 ------- 

744 value : `float` 

745 The result of the statistic calculated from the unflagged pixels. 

746 """ 

747 statObj = afwMath.makeStatistics(image, mask, statistic, statsCtrl) 

748 return statObj.getValue(statistic) 

749 

750 

751def setup_module(module): 

752 lsst.utils.tests.init() 

753 

754 

755class MemoryTestCase(lsst.utils.tests.MemoryTestCase): 

756 pass 

757 

758 

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

760 lsst.utils.tests.init() 

761 unittest.main()