Coverage for tests/test_subtractTask.py: 10%

417 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-29 03:01 -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 config.validate() 

159 self.assertEqual(config.mode, "convolveTemplate") 

160 

161 def test_mismatched_template(self): 

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

163 does not fully contain the science image. 

164 """ 

165 xSize = 200 

166 ySize = 200 

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

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

169 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

170 task = subtractImages.AlardLuptonSubtractTask(config=config) 

171 with self.assertRaises(AssertionError): 

172 task.run(template, science, sources) 

173 

174 def test_equal_images(self): 

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

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

177 """ 

178 noiseLevel = 1. 

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

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

181 templateBorderSize=20, doApplyCalibration=True) 

182 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

183 config.doSubtractBackground = False 

184 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

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

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

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

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

196 _makeStats(), statistic=afwMath.STDEV) 

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

198 

199 def test_auto_convolveTemplate(self): 

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

201 the template psf is the smaller. 

202 """ 

203 noiseLevel = 1. 

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

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

206 templateBorderSize=20, doApplyCalibration=True) 

207 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

208 config.doSubtractBackground = False 

209 config.mode = "convolveTemplate" 

210 config.forceCompatibility = False 

211 

212 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

214 

215 config.mode = "auto" 

216 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

219 

220 def test_auto_convolveScience(self): 

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

222 the science psf is the smaller. 

223 """ 

224 noiseLevel = 1. 

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

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

227 templateBorderSize=20, doApplyCalibration=True) 

228 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

229 config.doSubtractBackground = False 

230 config.mode = "convolveScience" 

231 config.forceCompatibility = False 

232 

233 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

235 

236 config.mode = "auto" 

237 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

240 

241 def test_science_better(self): 

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

243 with the science psf being smaller than the template. 

244 """ 

245 statsCtrl = _makeStats() 

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

247 

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

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

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

251 templateBorderSize=20, doApplyCalibration=True) 

252 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

253 config.doSubtractBackground = False 

254 config.forceCompatibility = False 

255 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

263 statsCtrlDetect) 

264 

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

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

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

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

269 statsCtrl) 

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

271 statsCtrl, statistic=afwMath.STDEV) 

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

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

274 

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

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

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

278 

279 def test_template_better(self): 

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

281 with the template psf being smaller than the science. 

282 """ 

283 statsCtrl = _makeStats() 

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

285 

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

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

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

289 templateBorderSize=20, doApplyCalibration=True) 

290 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

291 config.doSubtractBackground = False 

292 config.forceCompatibility = False 

293 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

301 

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

303 statsCtrlDetect) 

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

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

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

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

308 statsCtrl) 

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

310 statsCtrl, statistic=afwMath.STDEV) 

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

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

313 

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

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

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

317 

318 def test_symmetry(self): 

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

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

321 should be nearly the same. 

322 """ 

323 noiseLevel = 1. 

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

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

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

327 noiseSeed=6, templateBorderSize=0) 

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

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

330 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

331 config.mode = 'auto' 

332 config.doSubtractBackground = False 

333 config.forceCompatibility = False 

334 task = subtractImages.AlardLuptonSubtractTask(config=config) 

335 

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

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

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

339 

340 delta = template_better.difference.clone() 

341 delta.image -= science_better.difference.image 

342 delta.variance -= science_better.difference.variance 

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

344 

345 statsCtrl = _makeStats() 

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

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

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

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

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

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

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

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

354 

355 def test_few_sources(self): 

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

357 """ 

358 xSize = 256 

359 ySize = 256 

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

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

362 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

363 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

366 task.run(template, science, sources) 

367 

368 def test_order_equal_images(self): 

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

370 if the images are equivalent. 

371 """ 

372 noiseLevel = .1 

373 seed1 = 6 

374 seed2 = 7 

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

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

377 templateBorderSize=0, doApplyCalibration=True) 

378 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

379 config1.mode = "convolveTemplate" 

380 config1.doSubtractBackground = False 

381 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

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

383 

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

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

386 templateBorderSize=0, doApplyCalibration=True) 

387 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

388 config2.mode = "convolveScience" 

389 config2.doSubtractBackground = False 

390 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

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

392 diff1 = science1.maskedImage.clone() 

393 diff1 -= template1.maskedImage 

394 diff2 = science2.maskedImage.clone() 

395 diff2 -= template2.maskedImage 

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

397 diff1.image.array, 

398 atol=noiseLevel*5.) 

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

400 diff2.image.array, 

401 atol=noiseLevel*5.) 

402 diffErr = noiseLevel*2 

403 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage, 

404 results_convolveScience.difference.maskedImage, 

405 atol=diffErr*5.) 

406 

407 def test_background_subtraction(self): 

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

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

410 """ 

411 noiseLevel = 1. 

412 xSize = 512 

413 ySize = 512 

414 x0 = 123 

415 y0 = 456 

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

417 templateBorderSize=20, 

418 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

419 doApplyCalibration=True) 

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

421 

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

423 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

425 background=background_model, 

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

427 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

428 config.doSubtractBackground = True 

429 config.forceCompatibility = False 

430 

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

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

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

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

435 statsCtrl = _makeStats() 

436 

437 def _run_and_check_images(config, statsCtrl, mode): 

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

439 """ 

440 config.mode = mode 

441 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

443 

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

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

446 

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

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

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

450 

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

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

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

454 statsCtrl, statistic=afwMath.STDEV) 

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

456 

457 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

458 _run_and_check_images(config, statsCtrl, "convolveScience") 

459 

460 def test_scale_variance_convolve_template(self): 

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

462 """ 

463 scienceNoiseLevel = 4. 

464 templateNoiseLevel = 2. 

465 scaleFactor = 1.345 

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

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

468 

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

470 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

473 """ 

474 

475 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

476 config.doSubtractBackground = False 

477 config.forceCompatibility = False 

478 config.doDecorrelation = doDecorrelation 

479 config.doScaleVariance = doScaleVariance 

480 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

482 if doScaleVariance: 

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

484 scaleFactor, atol=0.05) 

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

486 scaleFactor, atol=0.05) 

487 

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

489 if doDecorrelation: 

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

491 else: 

492 templateNoise = _computeRobustStatistics(output.matchedTemplate.variance, 

493 output.matchedTemplate.mask, 

494 statsCtrl) 

495 

496 if doScaleVariance: 

497 templateNoise *= scaleFactor 

498 scienceNoise *= scaleFactor 

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

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

501 

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

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

504 templateBorderSize=20, doApplyCalibration=True) 

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

506 # when the template and science variance planes are correct 

507 _run_and_check_images(science, template, sources, statsCtrl, 

508 doDecorrelation=True, doScaleVariance=True) 

509 _run_and_check_images(science, template, sources, statsCtrl, 

510 doDecorrelation=True, doScaleVariance=False) 

511 _run_and_check_images(science, template, sources, statsCtrl, 

512 doDecorrelation=False, doScaleVariance=True) 

513 _run_and_check_images(science, template, sources, statsCtrl, 

514 doDecorrelation=False, doScaleVariance=False) 

515 

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

517 # when the template variance plane is incorrect 

518 template.variance.array /= scaleFactor 

519 science.variance.array /= scaleFactor 

520 _run_and_check_images(science, template, sources, statsCtrl, 

521 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

522 _run_and_check_images(science, template, sources, statsCtrl, 

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

524 _run_and_check_images(science, template, sources, statsCtrl, 

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

526 _run_and_check_images(science, template, sources, statsCtrl, 

527 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

528 

529 def test_scale_variance_convolve_science(self): 

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

531 """ 

532 scienceNoiseLevel = 4. 

533 templateNoiseLevel = 2. 

534 scaleFactor = 1.345 

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

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

537 

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

539 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

542 """ 

543 

544 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

545 config.doSubtractBackground = False 

546 config.forceCompatibility = False 

547 config.doDecorrelation = doDecorrelation 

548 config.doScaleVariance = doScaleVariance 

549 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

551 if doScaleVariance: 

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

553 scaleFactor, atol=0.05) 

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

555 scaleFactor, atol=0.05) 

556 

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

558 if doDecorrelation: 

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

560 else: 

561 scienceNoise = _computeRobustStatistics(output.matchedScience.variance, 

562 output.matchedScience.mask, 

563 statsCtrl) 

564 

565 if doScaleVariance: 

566 templateNoise *= scaleFactor 

567 scienceNoise *= scaleFactor 

568 

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

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

571 

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

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

574 templateBorderSize=20, doApplyCalibration=True) 

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

576 # when the template and science variance planes are correct 

577 _run_and_check_images(science, template, sources, statsCtrl, 

578 doDecorrelation=True, doScaleVariance=True) 

579 _run_and_check_images(science, template, sources, statsCtrl, 

580 doDecorrelation=True, doScaleVariance=False) 

581 _run_and_check_images(science, template, sources, statsCtrl, 

582 doDecorrelation=False, doScaleVariance=True) 

583 _run_and_check_images(science, template, sources, statsCtrl, 

584 doDecorrelation=False, doScaleVariance=False) 

585 

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

587 # when the template and science variance planes are incorrect 

588 science.variance.array /= scaleFactor 

589 template.variance.array /= scaleFactor 

590 _run_and_check_images(science, template, sources, statsCtrl, 

591 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

592 _run_and_check_images(science, template, sources, statsCtrl, 

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

594 _run_and_check_images(science, template, sources, statsCtrl, 

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

596 _run_and_check_images(science, template, sources, statsCtrl, 

597 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

598 

599 def test_exposure_properties_convolve_template(self): 

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

601 when the template is convolved. 

602 """ 

603 noiseLevel = 1. 

604 seed = 37 

605 rng = np.random.RandomState(seed) 

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

607 psf = science.psf 

608 psfAvgPos = psf.getAveragePosition() 

609 psfSize = getPsfFwhm(science.psf) 

610 psfImg = psf.computeKernelImage(psfAvgPos) 

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

612 templateBorderSize=20, doApplyCalibration=True) 

613 

614 # Generate a random aperture correction map 

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

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

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

618 science.info.setApCorrMap(apCorrMap) 

619 

620 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

621 config.mode = "convolveTemplate" 

622 config.forceCompatibility = False 

623 

624 def _run_and_check_images(doDecorrelation): 

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

626 """ 

627 config.doDecorrelation = doDecorrelation 

628 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

630 psfOut = output.difference.psf 

631 psfAvgPos = psfOut.getAveragePosition() 

632 if doDecorrelation: 

633 # Decorrelation requires recalculating the PSF, 

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

635 psfOutSize = getPsfFwhm(science.psf) 

636 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

637 else: 

638 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

639 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

640 

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

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

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

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

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

646 _run_and_check_images(doDecorrelation=True) 

647 _run_and_check_images(doDecorrelation=False) 

648 

649 def test_exposure_properties_convolve_science(self): 

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

651 when the science image is convolved. 

652 """ 

653 noiseLevel = 1. 

654 seed = 37 

655 rng = np.random.RandomState(seed) 

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

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

658 templateBorderSize=20, doApplyCalibration=True) 

659 psf = template.psf 

660 psfAvgPos = psf.getAveragePosition() 

661 psfSize = getPsfFwhm(template.psf) 

662 psfImg = psf.computeKernelImage(psfAvgPos) 

663 

664 # Generate a random aperture correction map 

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

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

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

668 science.info.setApCorrMap(apCorrMap) 

669 

670 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

671 config.mode = "convolveScience" 

672 config.forceCompatibility = False 

673 

674 def _run_and_check_images(doDecorrelation): 

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

676 """ 

677 config.doDecorrelation = doDecorrelation 

678 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

680 if doDecorrelation: 

681 # Decorrelation requires recalculating the PSF, 

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

683 psfOutSize = getPsfFwhm(template.psf) 

684 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

685 else: 

686 psfOut = output.difference.psf 

687 psfAvgPos = psfOut.getAveragePosition() 

688 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

689 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

690 

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

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

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

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

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

696 

697 _run_and_check_images(doDecorrelation=True) 

698 _run_and_check_images(doDecorrelation=False) 

699 

700 def _compare_apCorrMaps(self, a, b): 

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

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

703 

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

705 

706 Parameters 

707 ---------- 

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

709 The two aperture correction maps to compare. 

710 """ 

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

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

713 value2 = b.get(name) 

714 self.assertIsNotNone(value2) 

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

716 self.assertFloatsAlmostEqual( 

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

718 

719 

720def _makeStats(badMaskPlanes=None): 

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

722 

723 Parameters 

724 ---------- 

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

726 List of mask planes to exclude from calculations. 

727 

728 Returns 

729 ------- 

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

731 Statistics control object for configuring calculations on images. 

732 """ 

733 if badMaskPlanes is None: 

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

735 "BAD", "NO_DATA", "DETECTED_NEGATIVE") 

736 statsControl = afwMath.StatisticsControl() 

737 statsControl.setNumSigmaClip(3.) 

738 statsControl.setNumIter(3) 

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

740 return statsControl 

741 

742 

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

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

745 

746 Parameters 

747 ---------- 

748 image : `lsst.afw.image.Image` 

749 Image or variance plane of an exposure to evaluate. 

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

751 Mask plane to use for excluding pixels. 

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

753 Statistics control object for configuring the calculation. 

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

755 The type of statistic to compute. Typical values are 

756 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``. 

757 

758 Returns 

759 ------- 

760 value : `float` 

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

762 """ 

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

764 return statObj.getValue(statistic) 

765 

766 

767def setup_module(module): 

768 lsst.utils.tests.init() 

769 

770 

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

772 pass 

773 

774 

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

776 lsst.utils.tests.init() 

777 unittest.main()