Coverage for tests/test_subtractTask.py: 9%

419 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-15 03:29 -0700

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_config_validate_forceCompatibility(self): 

153 """Check that forceCompatibility sets `mode=convolveTemplate`. 

154 """ 

155 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

156 config.mode = "auto" 

157 config.forceCompatibility = True 

158 # Ensure we're not trying to change config values inside validate(). 

159 config.freeze() 

160 with self.assertRaises(FieldValidationError): 

161 config.validate() 

162 

163 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

164 config.mode = "convolveTemplate" 

165 config.forceCompatibility = True 

166 # Ensure we're not trying to change config values inside validate(). 

167 config.freeze() 

168 # Should not raise: 

169 config.validate() 

170 

171 def test_mismatched_template(self): 

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

173 does not fully contain the science image. 

174 """ 

175 xSize = 200 

176 ySize = 200 

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

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

179 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

180 task = subtractImages.AlardLuptonSubtractTask(config=config) 

181 with self.assertRaises(AssertionError): 

182 task.run(template, science, sources) 

183 

184 def test_equal_images(self): 

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

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

187 """ 

188 noiseLevel = 1. 

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

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

191 templateBorderSize=20, doApplyCalibration=True) 

192 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

193 config.doSubtractBackground = False 

194 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

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

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

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

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

206 _makeStats(), statistic=afwMath.STDEV) 

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

208 

209 def test_auto_convolveTemplate(self): 

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

211 the template psf is the smaller. 

212 """ 

213 noiseLevel = 1. 

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

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

216 templateBorderSize=20, doApplyCalibration=True) 

217 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

218 config.doSubtractBackground = False 

219 config.mode = "convolveTemplate" 

220 

221 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

223 

224 config.mode = "auto" 

225 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

228 

229 def test_auto_convolveScience(self): 

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

231 the science psf is the smaller. 

232 """ 

233 noiseLevel = 1. 

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

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

236 templateBorderSize=20, doApplyCalibration=True) 

237 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

238 config.doSubtractBackground = False 

239 config.mode = "convolveScience" 

240 

241 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

243 

244 config.mode = "auto" 

245 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

248 

249 def test_science_better(self): 

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

251 with the science psf being smaller than the template. 

252 """ 

253 statsCtrl = _makeStats() 

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

255 

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

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

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

259 templateBorderSize=20, doApplyCalibration=True) 

260 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

261 config.doSubtractBackground = False 

262 config.forceCompatibility = False 

263 config.mode = "convolveScience" 

264 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

272 statsCtrlDetect) 

273 

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

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

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

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

278 statsCtrl) 

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

280 statsCtrl, statistic=afwMath.STDEV) 

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

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

283 

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

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

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

287 

288 def test_template_better(self): 

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

290 with the template psf being smaller than the science. 

291 """ 

292 statsCtrl = _makeStats() 

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

294 

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

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

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

298 templateBorderSize=20, doApplyCalibration=True) 

299 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

300 config.doSubtractBackground = False 

301 config.forceCompatibility = False 

302 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

310 

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

312 statsCtrlDetect) 

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

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

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

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

317 statsCtrl) 

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

319 statsCtrl, statistic=afwMath.STDEV) 

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

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

322 

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

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

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

326 

327 def test_symmetry(self): 

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

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

330 should be nearly the same. 

331 """ 

332 noiseLevel = 1. 

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

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

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

336 noiseSeed=6, templateBorderSize=0) 

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

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

339 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

340 config.mode = 'auto' 

341 config.doSubtractBackground = False 

342 task = subtractImages.AlardLuptonSubtractTask(config=config) 

343 

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

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

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

347 

348 delta = template_better.difference.clone() 

349 delta.image -= science_better.difference.image 

350 delta.variance -= science_better.difference.variance 

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

352 

353 statsCtrl = _makeStats() 

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

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

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

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

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

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

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

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

362 

363 def test_few_sources(self): 

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

365 """ 

366 xSize = 256 

367 ySize = 256 

368 science, sources = makeTestImage(psfSize=2.4, nSrc=1, xSize=xSize, ySize=ySize) 

369 template, _ = makeTestImage(psfSize=2.0, nSrc=1, xSize=xSize, ySize=ySize, doApplyCalibration=True) 

370 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

371 task = subtractImages.AlardLuptonSubtractTask(config=config) 

372 with self.assertRaisesRegex(lsst.pex.exceptions.Exception, 

373 'Unable to determine kernel sum; 0 candidates'): 

374 task.run(template, science, sources) 

375 

376 def test_order_equal_images(self): 

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

378 if the images are equivalent. 

379 """ 

380 noiseLevel = .1 

381 seed1 = 6 

382 seed2 = 7 

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

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

385 templateBorderSize=0, doApplyCalibration=True) 

386 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

387 config1.mode = "convolveTemplate" 

388 config1.doSubtractBackground = False 

389 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

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

391 

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

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

394 templateBorderSize=0, doApplyCalibration=True) 

395 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

396 config2.mode = "convolveScience" 

397 config2.doSubtractBackground = False 

398 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

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

400 diff1 = science1.maskedImage.clone() 

401 diff1 -= template1.maskedImage 

402 diff2 = science2.maskedImage.clone() 

403 diff2 -= template2.maskedImage 

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

405 diff1.image.array, 

406 atol=noiseLevel*5.) 

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

408 diff2.image.array, 

409 atol=noiseLevel*5.) 

410 diffErr = noiseLevel*2 

411 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage, 

412 results_convolveScience.difference.maskedImage, 

413 atol=diffErr*5.) 

414 

415 def test_background_subtraction(self): 

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

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

418 """ 

419 noiseLevel = 1. 

420 xSize = 512 

421 ySize = 512 

422 x0 = 123 

423 y0 = 456 

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

425 templateBorderSize=20, 

426 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

427 doApplyCalibration=True) 

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

429 

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

431 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

433 background=background_model, 

434 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

435 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

436 config.doSubtractBackground = True 

437 

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

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

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

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

442 statsCtrl = _makeStats() 

443 

444 def _run_and_check_images(config, statsCtrl, mode): 

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

446 """ 

447 config.mode = mode 

448 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

450 

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

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

453 

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

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

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

457 

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

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

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

461 statsCtrl, statistic=afwMath.STDEV) 

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

463 

464 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

465 _run_and_check_images(config, statsCtrl, "convolveScience") 

466 

467 def test_scale_variance_convolve_template(self): 

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

469 """ 

470 scienceNoiseLevel = 4. 

471 templateNoiseLevel = 2. 

472 scaleFactor = 1.345 

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

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

475 

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

477 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

480 """ 

481 

482 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

483 config.doSubtractBackground = False 

484 config.forceCompatibility = False 

485 config.doDecorrelation = doDecorrelation 

486 config.doScaleVariance = doScaleVariance 

487 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

489 if doScaleVariance: 

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

491 scaleFactor, atol=0.05) 

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

493 scaleFactor, atol=0.05) 

494 

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

496 if doDecorrelation: 

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

498 else: 

499 templateNoise = _computeRobustStatistics(output.matchedTemplate.variance, 

500 output.matchedTemplate.mask, 

501 statsCtrl) 

502 

503 if doScaleVariance: 

504 templateNoise *= scaleFactor 

505 scienceNoise *= scaleFactor 

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

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

508 

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

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

511 templateBorderSize=20, doApplyCalibration=True) 

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

513 # when the template and science variance planes are correct 

514 _run_and_check_images(science, template, sources, statsCtrl, 

515 doDecorrelation=True, doScaleVariance=True) 

516 _run_and_check_images(science, template, sources, statsCtrl, 

517 doDecorrelation=True, doScaleVariance=False) 

518 _run_and_check_images(science, template, sources, statsCtrl, 

519 doDecorrelation=False, doScaleVariance=True) 

520 _run_and_check_images(science, template, sources, statsCtrl, 

521 doDecorrelation=False, doScaleVariance=False) 

522 

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

524 # when the template variance plane is incorrect 

525 template.variance.array /= scaleFactor 

526 science.variance.array /= scaleFactor 

527 _run_and_check_images(science, template, sources, statsCtrl, 

528 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

529 _run_and_check_images(science, template, sources, statsCtrl, 

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

531 _run_and_check_images(science, template, sources, statsCtrl, 

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

533 _run_and_check_images(science, template, sources, statsCtrl, 

534 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

535 

536 def test_scale_variance_convolve_science(self): 

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

538 """ 

539 scienceNoiseLevel = 4. 

540 templateNoiseLevel = 2. 

541 scaleFactor = 1.345 

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

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

544 

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

546 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

549 """ 

550 

551 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

552 config.mode = "convolveScience" 

553 config.doSubtractBackground = False 

554 config.forceCompatibility = False 

555 config.doDecorrelation = doDecorrelation 

556 config.doScaleVariance = doScaleVariance 

557 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

559 if doScaleVariance: 

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

561 scaleFactor, atol=0.05) 

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

563 scaleFactor, atol=0.05) 

564 

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

566 if doDecorrelation: 

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

568 else: 

569 scienceNoise = _computeRobustStatistics(output.matchedScience.variance, 

570 output.matchedScience.mask, 

571 statsCtrl) 

572 

573 if doScaleVariance: 

574 templateNoise *= scaleFactor 

575 scienceNoise *= scaleFactor 

576 

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

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

579 

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

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

582 templateBorderSize=20, doApplyCalibration=True) 

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

584 # when the template and science variance planes are correct 

585 _run_and_check_images(science, template, sources, statsCtrl, 

586 doDecorrelation=True, doScaleVariance=True) 

587 _run_and_check_images(science, template, sources, statsCtrl, 

588 doDecorrelation=True, doScaleVariance=False) 

589 _run_and_check_images(science, template, sources, statsCtrl, 

590 doDecorrelation=False, doScaleVariance=True) 

591 _run_and_check_images(science, template, sources, statsCtrl, 

592 doDecorrelation=False, doScaleVariance=False) 

593 

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

595 # when the template and science variance planes are incorrect 

596 science.variance.array /= scaleFactor 

597 template.variance.array /= scaleFactor 

598 _run_and_check_images(science, template, sources, statsCtrl, 

599 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

600 _run_and_check_images(science, template, sources, statsCtrl, 

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

602 _run_and_check_images(science, template, sources, statsCtrl, 

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

604 _run_and_check_images(science, template, sources, statsCtrl, 

605 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

606 

607 def test_exposure_properties_convolve_template(self): 

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

609 when the template is convolved. 

610 """ 

611 noiseLevel = 1. 

612 seed = 37 

613 rng = np.random.RandomState(seed) 

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

615 psf = science.psf 

616 psfAvgPos = psf.getAveragePosition() 

617 psfSize = getPsfFwhm(science.psf) 

618 psfImg = psf.computeKernelImage(psfAvgPos) 

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

620 templateBorderSize=20, doApplyCalibration=True) 

621 

622 # Generate a random aperture correction map 

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

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

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

626 science.info.setApCorrMap(apCorrMap) 

627 

628 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

629 config.mode = "convolveTemplate" 

630 

631 def _run_and_check_images(doDecorrelation): 

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

633 """ 

634 config.doDecorrelation = doDecorrelation 

635 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

637 psfOut = output.difference.psf 

638 psfAvgPos = psfOut.getAveragePosition() 

639 if doDecorrelation: 

640 # Decorrelation requires recalculating the PSF, 

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

642 psfOutSize = getPsfFwhm(science.psf) 

643 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

644 else: 

645 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

646 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

647 

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

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

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

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

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

653 _run_and_check_images(doDecorrelation=True) 

654 _run_and_check_images(doDecorrelation=False) 

655 

656 def test_exposure_properties_convolve_science(self): 

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

658 when the science image is convolved. 

659 """ 

660 noiseLevel = 1. 

661 seed = 37 

662 rng = np.random.RandomState(seed) 

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

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

665 templateBorderSize=20, doApplyCalibration=True) 

666 psf = template.psf 

667 psfAvgPos = psf.getAveragePosition() 

668 psfSize = getPsfFwhm(template.psf) 

669 psfImg = psf.computeKernelImage(psfAvgPos) 

670 

671 # Generate a random aperture correction map 

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

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

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

675 science.info.setApCorrMap(apCorrMap) 

676 

677 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

678 config.mode = "convolveScience" 

679 

680 def _run_and_check_images(doDecorrelation): 

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

682 """ 

683 config.doDecorrelation = doDecorrelation 

684 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

686 if doDecorrelation: 

687 # Decorrelation requires recalculating the PSF, 

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

689 psfOutSize = getPsfFwhm(template.psf) 

690 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

691 else: 

692 psfOut = output.difference.psf 

693 psfAvgPos = psfOut.getAveragePosition() 

694 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

695 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

696 

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

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

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

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

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

702 

703 _run_and_check_images(doDecorrelation=True) 

704 _run_and_check_images(doDecorrelation=False) 

705 

706 def _compare_apCorrMaps(self, a, b): 

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

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

709 

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

711 

712 Parameters 

713 ---------- 

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

715 The two aperture correction maps to compare. 

716 """ 

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

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

719 value2 = b.get(name) 

720 self.assertIsNotNone(value2) 

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

722 self.assertFloatsAlmostEqual( 

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

724 

725 

726def _makeStats(badMaskPlanes=None): 

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

728 

729 Parameters 

730 ---------- 

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

732 List of mask planes to exclude from calculations. 

733 

734 Returns 

735 ------- 

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

737 Statistics control object for configuring calculations on images. 

738 """ 

739 if badMaskPlanes is None: 

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

741 "BAD", "NO_DATA", "DETECTED_NEGATIVE") 

742 statsControl = afwMath.StatisticsControl() 

743 statsControl.setNumSigmaClip(3.) 

744 statsControl.setNumIter(3) 

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

746 return statsControl 

747 

748 

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

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

751 

752 Parameters 

753 ---------- 

754 image : `lsst.afw.image.Image` 

755 Image or variance plane of an exposure to evaluate. 

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

757 Mask plane to use for excluding pixels. 

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

759 Statistics control object for configuring the calculation. 

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

761 The type of statistic to compute. Typical values are 

762 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``. 

763 

764 Returns 

765 ------- 

766 value : `float` 

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

768 """ 

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

770 return statObj.getValue(statistic) 

771 

772 

773def setup_module(module): 

774 lsst.utils.tests.init() 

775 

776 

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

778 pass 

779 

780 

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

782 lsst.utils.tests.init() 

783 unittest.main()