Coverage for tests/test_subtractTask.py: 9%

420 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-09 03:42 -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_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=10, xSize=xSize, ySize=ySize) 

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

370 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

371 task = subtractImages.AlardLuptonSubtractTask(config=config) 

372 sources = sources[0:1] 

373 with self.assertRaisesRegex(RuntimeError, 

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

375 task.run(template, science, sources) 

376 

377 def test_order_equal_images(self): 

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

379 if the images are equivalent. 

380 """ 

381 noiseLevel = .1 

382 seed1 = 6 

383 seed2 = 7 

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

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

386 templateBorderSize=0, doApplyCalibration=True) 

387 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

388 config1.mode = "convolveTemplate" 

389 config1.doSubtractBackground = False 

390 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

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

392 

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

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

395 templateBorderSize=0, doApplyCalibration=True) 

396 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

397 config2.mode = "convolveScience" 

398 config2.doSubtractBackground = False 

399 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

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

401 diff1 = science1.maskedImage.clone() 

402 diff1 -= template1.maskedImage 

403 diff2 = science2.maskedImage.clone() 

404 diff2 -= template2.maskedImage 

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

406 diff1.image.array, 

407 atol=noiseLevel*5.) 

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

409 diff2.image.array, 

410 atol=noiseLevel*5.) 

411 diffErr = noiseLevel*2 

412 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage, 

413 results_convolveScience.difference.maskedImage, 

414 atol=diffErr*5.) 

415 

416 def test_background_subtraction(self): 

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

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

419 """ 

420 noiseLevel = 1. 

421 xSize = 512 

422 ySize = 512 

423 x0 = 123 

424 y0 = 456 

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

426 templateBorderSize=20, 

427 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

428 doApplyCalibration=True) 

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

430 

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

432 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

434 background=background_model, 

435 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

436 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

437 config.doSubtractBackground = True 

438 

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

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

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

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

443 statsCtrl = _makeStats() 

444 

445 def _run_and_check_images(config, statsCtrl, mode): 

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

447 """ 

448 config.mode = mode 

449 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

451 

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

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

454 

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

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

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

458 

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

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

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

462 statsCtrl, statistic=afwMath.STDEV) 

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

464 

465 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

466 _run_and_check_images(config, statsCtrl, "convolveScience") 

467 

468 def test_scale_variance_convolve_template(self): 

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

470 """ 

471 scienceNoiseLevel = 4. 

472 templateNoiseLevel = 2. 

473 scaleFactor = 1.345 

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

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

476 

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

478 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

481 """ 

482 

483 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

484 config.doSubtractBackground = False 

485 config.forceCompatibility = False 

486 config.doDecorrelation = doDecorrelation 

487 config.doScaleVariance = doScaleVariance 

488 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

490 if doScaleVariance: 

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

492 scaleFactor, atol=0.05) 

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

494 scaleFactor, atol=0.05) 

495 

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

497 if doDecorrelation: 

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

499 else: 

500 templateNoise = _computeRobustStatistics(output.matchedTemplate.variance, 

501 output.matchedTemplate.mask, 

502 statsCtrl) 

503 

504 if doScaleVariance: 

505 templateNoise *= scaleFactor 

506 scienceNoise *= scaleFactor 

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

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

509 

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

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

512 templateBorderSize=20, doApplyCalibration=True) 

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

514 # when the template and science variance planes are correct 

515 _run_and_check_images(science, template, sources, statsCtrl, 

516 doDecorrelation=True, doScaleVariance=True) 

517 _run_and_check_images(science, template, sources, statsCtrl, 

518 doDecorrelation=True, doScaleVariance=False) 

519 _run_and_check_images(science, template, sources, statsCtrl, 

520 doDecorrelation=False, doScaleVariance=True) 

521 _run_and_check_images(science, template, sources, statsCtrl, 

522 doDecorrelation=False, doScaleVariance=False) 

523 

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

525 # when the template variance plane is incorrect 

526 template.variance.array /= scaleFactor 

527 science.variance.array /= scaleFactor 

528 _run_and_check_images(science, template, sources, statsCtrl, 

529 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

530 _run_and_check_images(science, template, sources, statsCtrl, 

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

532 _run_and_check_images(science, template, sources, statsCtrl, 

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

534 _run_and_check_images(science, template, sources, statsCtrl, 

535 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

536 

537 def test_scale_variance_convolve_science(self): 

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

539 """ 

540 scienceNoiseLevel = 4. 

541 templateNoiseLevel = 2. 

542 scaleFactor = 1.345 

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

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

545 

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

547 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

550 """ 

551 

552 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

553 config.mode = "convolveScience" 

554 config.doSubtractBackground = False 

555 config.forceCompatibility = False 

556 config.doDecorrelation = doDecorrelation 

557 config.doScaleVariance = doScaleVariance 

558 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

560 if doScaleVariance: 

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

562 scaleFactor, atol=0.05) 

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

564 scaleFactor, atol=0.05) 

565 

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

567 if doDecorrelation: 

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

569 else: 

570 scienceNoise = _computeRobustStatistics(output.matchedScience.variance, 

571 output.matchedScience.mask, 

572 statsCtrl) 

573 

574 if doScaleVariance: 

575 templateNoise *= scaleFactor 

576 scienceNoise *= scaleFactor 

577 

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

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

580 

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

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

583 templateBorderSize=20, doApplyCalibration=True) 

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

585 # when the template and science variance planes are correct 

586 _run_and_check_images(science, template, sources, statsCtrl, 

587 doDecorrelation=True, doScaleVariance=True) 

588 _run_and_check_images(science, template, sources, statsCtrl, 

589 doDecorrelation=True, doScaleVariance=False) 

590 _run_and_check_images(science, template, sources, statsCtrl, 

591 doDecorrelation=False, doScaleVariance=True) 

592 _run_and_check_images(science, template, sources, statsCtrl, 

593 doDecorrelation=False, doScaleVariance=False) 

594 

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

596 # when the template and science variance planes are incorrect 

597 science.variance.array /= scaleFactor 

598 template.variance.array /= scaleFactor 

599 _run_and_check_images(science, template, sources, statsCtrl, 

600 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

601 _run_and_check_images(science, template, sources, statsCtrl, 

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

603 _run_and_check_images(science, template, sources, statsCtrl, 

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

605 _run_and_check_images(science, template, sources, statsCtrl, 

606 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

607 

608 def test_exposure_properties_convolve_template(self): 

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

610 when the template is convolved. 

611 """ 

612 noiseLevel = 1. 

613 seed = 37 

614 rng = np.random.RandomState(seed) 

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

616 psf = science.psf 

617 psfAvgPos = psf.getAveragePosition() 

618 psfSize = getPsfFwhm(science.psf) 

619 psfImg = psf.computeKernelImage(psfAvgPos) 

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

621 templateBorderSize=20, doApplyCalibration=True) 

622 

623 # Generate a random aperture correction map 

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

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

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

627 science.info.setApCorrMap(apCorrMap) 

628 

629 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

630 config.mode = "convolveTemplate" 

631 

632 def _run_and_check_images(doDecorrelation): 

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

634 """ 

635 config.doDecorrelation = doDecorrelation 

636 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

638 psfOut = output.difference.psf 

639 psfAvgPos = psfOut.getAveragePosition() 

640 if doDecorrelation: 

641 # Decorrelation requires recalculating the PSF, 

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

643 psfOutSize = getPsfFwhm(science.psf) 

644 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

645 else: 

646 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

647 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

648 

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

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

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

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

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

654 _run_and_check_images(doDecorrelation=True) 

655 _run_and_check_images(doDecorrelation=False) 

656 

657 def test_exposure_properties_convolve_science(self): 

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

659 when the science image is convolved. 

660 """ 

661 noiseLevel = 1. 

662 seed = 37 

663 rng = np.random.RandomState(seed) 

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

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

666 templateBorderSize=20, doApplyCalibration=True) 

667 psf = template.psf 

668 psfAvgPos = psf.getAveragePosition() 

669 psfSize = getPsfFwhm(template.psf) 

670 psfImg = psf.computeKernelImage(psfAvgPos) 

671 

672 # Generate a random aperture correction map 

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

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

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

676 science.info.setApCorrMap(apCorrMap) 

677 

678 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

679 config.mode = "convolveScience" 

680 

681 def _run_and_check_images(doDecorrelation): 

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

683 """ 

684 config.doDecorrelation = doDecorrelation 

685 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

687 if doDecorrelation: 

688 # Decorrelation requires recalculating the PSF, 

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

690 psfOutSize = getPsfFwhm(template.psf) 

691 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

692 else: 

693 psfOut = output.difference.psf 

694 psfAvgPos = psfOut.getAveragePosition() 

695 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

696 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

697 

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

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

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

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

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

703 

704 _run_and_check_images(doDecorrelation=True) 

705 _run_and_check_images(doDecorrelation=False) 

706 

707 def _compare_apCorrMaps(self, a, b): 

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

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

710 

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

712 

713 Parameters 

714 ---------- 

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

716 The two aperture correction maps to compare. 

717 """ 

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

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

720 value2 = b.get(name) 

721 self.assertIsNotNone(value2) 

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

723 self.assertFloatsAlmostEqual( 

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

725 

726 

727def _makeStats(badMaskPlanes=None): 

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

729 

730 Parameters 

731 ---------- 

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

733 List of mask planes to exclude from calculations. 

734 

735 Returns 

736 ------- 

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

738 Statistics control object for configuring calculations on images. 

739 """ 

740 if badMaskPlanes is None: 

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

742 "BAD", "NO_DATA", "DETECTED_NEGATIVE") 

743 statsControl = afwMath.StatisticsControl() 

744 statsControl.setNumSigmaClip(3.) 

745 statsControl.setNumIter(3) 

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

747 return statsControl 

748 

749 

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

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

752 

753 Parameters 

754 ---------- 

755 image : `lsst.afw.image.Image` 

756 Image or variance plane of an exposure to evaluate. 

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

758 Mask plane to use for excluding pixels. 

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

760 Statistics control object for configuring the calculation. 

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

762 The type of statistic to compute. Typical values are 

763 ``afwMath.MEANCLIP`` or ``afwMath.STDEVCLIP``. 

764 

765 Returns 

766 ------- 

767 value : `float` 

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

769 """ 

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

771 return statObj.getValue(statistic) 

772 

773 

774def setup_module(module): 

775 lsst.utils.tests.init() 

776 

777 

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

779 pass 

780 

781 

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

783 lsst.utils.tests.init() 

784 unittest.main()