Coverage for tests/test_subtractTask.py: 9%

393 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-07 11:36 +0000

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 

23 

24import lsst.afw.math as afwMath 

25import lsst.afw.table as afwTable 

26import lsst.geom 

27import lsst.ip.diffim.imagePsfMatch 

28import lsst.meas.algorithms as measAlg 

29import lsst.utils.tests 

30import numpy as np 

31from lsst.ip.diffim import subtractImages 

32from lsst.ip.diffim.utils import (computeRobustStatistics, evaluateMeanPsfFwhm, 

33 getPsfFwhm, makeStats, makeTestImage) 

34from lsst.pex.config import FieldValidationError 

35from lsst.pex.exceptions import InvalidParameterError 

36 

37 

38class CustomCoaddPsf(measAlg.CoaddPsf): 

39 """A custom CoaddPSF that overrides the getAveragePosition method. 

40 """ 

41 def getAveragePosition(self): 

42 return lsst.geom.Point2D(-10000, -10000) 

43 

44 

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

46 

47 def test_allowed_config_modes(self): 

48 """Verify the allowable modes for convolution. 

49 """ 

50 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

51 config.mode = 'auto' 

52 config.mode = 'convolveScience' 

53 config.mode = 'convolveTemplate' 

54 

55 with self.assertRaises(FieldValidationError): 

56 config.mode = 'aotu' 

57 

58 def test_mismatched_template(self): 

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

60 does not fully contain the science image. 

61 """ 

62 xSize = 200 

63 ySize = 200 

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

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

66 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

67 task = subtractImages.AlardLuptonSubtractTask(config=config) 

68 with self.assertRaises(AssertionError): 

69 task.run(template, science, sources) 

70 

71 def test_equal_images(self): 

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

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

74 """ 

75 noiseLevel = 1. 

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

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

78 templateBorderSize=20, doApplyCalibration=True) 

79 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

80 config.doSubtractBackground = False 

81 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

88 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

89 differenceMean = computeRobustStatistics(output.difference.image, output.difference.mask, statsCtrl) 

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

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

92 differenceStd = computeRobustStatistics(output.difference.image, output.difference.mask, 

93 makeStats(), statistic=afwMath.STDEV) 

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

95 

96 def test_psf_size(self): 

97 """Test that the image subtract task runs without failing, if 

98 fwhmExposureBuffer and fwhmExposureGrid parameters are set. 

99 """ 

100 noiseLevel = 1. 

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

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

103 templateBorderSize=20, doApplyCalibration=True) 

104 

105 schema = afwTable.ExposureTable.makeMinimalSchema() 

106 weightKey = schema.addField("weight", type="D", doc="Coadd weight") 

107 exposureCatalog = afwTable.ExposureCatalog(schema) 

108 kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel() 

109 psf = measAlg.KernelPsf(kernel, template.getBBox().getCenter()) 

110 

111 record = exposureCatalog.addNew() 

112 record.setPsf(psf) 

113 record.setWcs(template.wcs) 

114 record.setD(weightKey, 1.0) 

115 record.setBBox(template.getBBox()) 

116 

117 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs) 

118 template.setPsf(customPsf) 

119 

120 # Test that we get an exception if we simply get the FWHM at center. 

121 with self.assertRaises(InvalidParameterError): 

122 getPsfFwhm(template.psf, True) 

123 

124 with self.assertRaises(InvalidParameterError): 

125 getPsfFwhm(template.psf, False) 

126 

127 # Test that evaluateMeanPsfFwhm runs successfully on the template. 

128 evaluateMeanPsfFwhm(template, fwhmExposureBuffer=0.05, fwhmExposureGrid=10) 

129 

130 # Since the PSF is spatially invariant, the FWHM should be the same at 

131 # all points in the science image. 

132 fwhm1 = getPsfFwhm(science.psf, False) 

133 fwhm2 = evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05, fwhmExposureGrid=10) 

134 self.assertAlmostEqual(fwhm1[0], fwhm2, places=13) 

135 self.assertAlmostEqual(fwhm1[1], fwhm2, places=13) 

136 

137 self.assertAlmostEqual(evaluateMeanPsfFwhm(science, fwhmExposureBuffer=0.05, 

138 fwhmExposureGrid=10), 

139 getPsfFwhm(science.psf, True), places=7 

140 ) 

141 

142 # Test that the image subtraction task runs successfully. 

143 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

144 config.doSubtractBackground = False 

145 task = subtractImages.AlardLuptonSubtractTask(config=config) 

146 

147 # Test that the task runs if we take the mean FWHM on a grid. 

148 with self.assertLogs(level="INFO") as cm: 

149 task.run(template, science, sources) 

150 

151 # Check that evaluateMeanPsfFwhm was called. 

152 # This tests that getPsfFwhm failed raising InvalidParameterError, 

153 # that is caught and handled appropriately. 

154 logMessage = ("INFO:lsst.alardLuptonSubtract:Unable to evaluate PSF at the average position. " 

155 "Evaluting PSF on a grid of points." 

156 ) 

157 self.assertIn(logMessage, cm.output) 

158 

159 def test_auto_convolveTemplate(self): 

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

161 the template psf is the smaller. 

162 """ 

163 noiseLevel = 1. 

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

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

166 templateBorderSize=20, doApplyCalibration=True) 

167 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

168 config.doSubtractBackground = False 

169 config.mode = "convolveTemplate" 

170 

171 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

173 

174 config.mode = "auto" 

175 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

178 

179 def test_auto_convolveScience(self): 

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

181 the science psf is the smaller. 

182 """ 

183 noiseLevel = 1. 

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

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

186 templateBorderSize=20, doApplyCalibration=True) 

187 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

188 config.doSubtractBackground = False 

189 config.mode = "convolveScience" 

190 

191 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

193 

194 config.mode = "auto" 

195 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

198 

199 def test_science_better(self): 

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

201 with the science psf being smaller than the template. 

202 """ 

203 statsCtrl = makeStats() 

204 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

205 

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

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

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

209 templateBorderSize=20, doApplyCalibration=True) 

210 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

211 config.doSubtractBackground = False 

212 config.mode = "convolveScience" 

213 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

220 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask, 

221 statsCtrlDetect) 

222 

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

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

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

226 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask, 

227 statsCtrl) 

228 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask, 

229 statsCtrl, statistic=afwMath.STDEV) 

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

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

232 

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

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

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

236 

237 def test_template_better(self): 

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

239 with the template psf being smaller than the science. 

240 """ 

241 statsCtrl = makeStats() 

242 statsCtrlDetect = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

243 

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

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

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

247 templateBorderSize=20, doApplyCalibration=True) 

248 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

249 config.doSubtractBackground = False 

250 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

258 

259 diffimMean = computeRobustStatistics(output.difference.image, output.difference.mask, 

260 statsCtrlDetect) 

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

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

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

264 varianceMean = computeRobustStatistics(output.difference.variance, output.difference.mask, 

265 statsCtrl) 

266 diffimStd = computeRobustStatistics(output.difference.image, output.difference.mask, 

267 statsCtrl, statistic=afwMath.STDEV) 

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

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

270 

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

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

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

274 

275 def test_symmetry(self): 

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

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

278 should be nearly the same. 

279 """ 

280 noiseLevel = 1. 

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

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

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

284 noiseSeed=6, templateBorderSize=0) 

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

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

287 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

288 config.mode = 'auto' 

289 config.doSubtractBackground = False 

290 task = subtractImages.AlardLuptonSubtractTask(config=config) 

291 

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

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

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

295 

296 delta = template_better.difference.clone() 

297 delta.image -= science_better.difference.image 

298 delta.variance -= science_better.difference.variance 

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

300 

301 statsCtrl = makeStats() 

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

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

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

305 deltaMean = computeRobustStatistics(delta.image, delta.mask, statsCtrl) 

306 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl, statistic=afwMath.STDEV) 

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

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

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

310 

311 def test_few_sources(self): 

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

313 """ 

314 xSize = 256 

315 ySize = 256 

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

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

318 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

319 task = subtractImages.AlardLuptonSubtractTask(config=config) 

320 sources = sources[0:1] 

321 with self.assertRaisesRegex(RuntimeError, 

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

323 task.run(template, science, sources) 

324 

325 def test_order_equal_images(self): 

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

327 if the images are equivalent. 

328 """ 

329 noiseLevel = .1 

330 seed1 = 6 

331 seed2 = 7 

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

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

334 templateBorderSize=0, doApplyCalibration=True) 

335 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

336 config1.mode = "convolveTemplate" 

337 config1.doSubtractBackground = False 

338 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

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

340 

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

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

343 templateBorderSize=0, doApplyCalibration=True) 

344 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

345 config2.mode = "convolveScience" 

346 config2.doSubtractBackground = False 

347 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

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

349 diff1 = science1.maskedImage.clone() 

350 diff1 -= template1.maskedImage 

351 diff2 = science2.maskedImage.clone() 

352 diff2 -= template2.maskedImage 

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

354 diff1.image.array, 

355 atol=noiseLevel*5.) 

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

357 diff2.image.array, 

358 atol=noiseLevel*5.) 

359 diffErr = noiseLevel*2 

360 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference.maskedImage, 

361 results_convolveScience.difference.maskedImage, 

362 atol=diffErr*5.) 

363 

364 def test_background_subtraction(self): 

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

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

367 """ 

368 noiseLevel = 1. 

369 xSize = 512 

370 ySize = 512 

371 x0 = 123 

372 y0 = 456 

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

374 templateBorderSize=20, 

375 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

376 doApplyCalibration=True) 

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

378 

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

380 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

382 background=background_model, 

383 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

384 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

385 config.doSubtractBackground = True 

386 

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

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

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

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

391 statsCtrl = makeStats() 

392 

393 def _run_and_check_images(config, statsCtrl, mode): 

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

395 """ 

396 config.mode = mode 

397 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

399 

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

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

402 

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

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

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

406 

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

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

409 stdVal = computeRobustStatistics(output.difference.image, output.difference.mask, 

410 statsCtrl, statistic=afwMath.STDEV) 

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

412 

413 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

414 _run_and_check_images(config, statsCtrl, "convolveScience") 

415 

416 def test_scale_variance_convolve_template(self): 

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

418 """ 

419 scienceNoiseLevel = 4. 

420 templateNoiseLevel = 2. 

421 scaleFactor = 1.345 

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

423 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

424 

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

426 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

429 """ 

430 

431 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

432 config.doSubtractBackground = False 

433 config.doDecorrelation = doDecorrelation 

434 config.doScaleVariance = doScaleVariance 

435 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

437 if doScaleVariance: 

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

439 scaleFactor, atol=0.05) 

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

441 scaleFactor, atol=0.05) 

442 

443 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl) 

444 if doDecorrelation: 

445 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl) 

446 else: 

447 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

448 output.matchedTemplate.mask, 

449 statsCtrl) 

450 

451 if doScaleVariance: 

452 templateNoise *= scaleFactor 

453 scienceNoise *= scaleFactor 

454 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl) 

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

456 

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

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

459 templateBorderSize=20, doApplyCalibration=True) 

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

461 # when the template and science variance planes are correct 

462 _run_and_check_images(science, template, sources, statsCtrl, 

463 doDecorrelation=True, doScaleVariance=True) 

464 _run_and_check_images(science, template, sources, statsCtrl, 

465 doDecorrelation=True, doScaleVariance=False) 

466 _run_and_check_images(science, template, sources, statsCtrl, 

467 doDecorrelation=False, doScaleVariance=True) 

468 _run_and_check_images(science, template, sources, statsCtrl, 

469 doDecorrelation=False, doScaleVariance=False) 

470 

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

472 # when the template variance plane is incorrect 

473 template.variance.array /= scaleFactor 

474 science.variance.array /= scaleFactor 

475 _run_and_check_images(science, template, sources, statsCtrl, 

476 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

477 _run_and_check_images(science, template, sources, statsCtrl, 

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

479 _run_and_check_images(science, template, sources, statsCtrl, 

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

481 _run_and_check_images(science, template, sources, statsCtrl, 

482 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

483 

484 def test_scale_variance_convolve_science(self): 

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

486 """ 

487 scienceNoiseLevel = 4. 

488 templateNoiseLevel = 2. 

489 scaleFactor = 1.345 

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

491 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA")) 

492 

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

494 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

497 """ 

498 

499 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

500 config.mode = "convolveScience" 

501 config.doSubtractBackground = False 

502 config.doDecorrelation = doDecorrelation 

503 config.doScaleVariance = doScaleVariance 

504 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

506 if doScaleVariance: 

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

508 scaleFactor, atol=0.05) 

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

510 scaleFactor, atol=0.05) 

511 

512 templateNoise = computeRobustStatistics(template.variance, template.mask, statsCtrl) 

513 if doDecorrelation: 

514 scienceNoise = computeRobustStatistics(science.variance, science.mask, statsCtrl) 

515 else: 

516 scienceNoise = computeRobustStatistics(output.matchedScience.variance, 

517 output.matchedScience.mask, 

518 statsCtrl) 

519 

520 if doScaleVariance: 

521 templateNoise *= scaleFactor 

522 scienceNoise *= scaleFactor 

523 

524 varMean = computeRobustStatistics(output.difference.variance, output.difference.mask, statsCtrl) 

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

526 

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

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

529 templateBorderSize=20, doApplyCalibration=True) 

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

531 # when the template and science variance planes are correct 

532 _run_and_check_images(science, template, sources, statsCtrl, 

533 doDecorrelation=True, doScaleVariance=True) 

534 _run_and_check_images(science, template, sources, statsCtrl, 

535 doDecorrelation=True, doScaleVariance=False) 

536 _run_and_check_images(science, template, sources, statsCtrl, 

537 doDecorrelation=False, doScaleVariance=True) 

538 _run_and_check_images(science, template, sources, statsCtrl, 

539 doDecorrelation=False, doScaleVariance=False) 

540 

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

542 # when the template and science variance planes are incorrect 

543 science.variance.array /= scaleFactor 

544 template.variance.array /= scaleFactor 

545 _run_and_check_images(science, template, sources, statsCtrl, 

546 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

547 _run_and_check_images(science, template, sources, statsCtrl, 

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

549 _run_and_check_images(science, template, sources, statsCtrl, 

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

551 _run_and_check_images(science, template, sources, statsCtrl, 

552 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

553 

554 def test_exposure_properties_convolve_template(self): 

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

556 when the template is convolved. 

557 """ 

558 noiseLevel = 1. 

559 seed = 37 

560 rng = np.random.RandomState(seed) 

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

562 psf = science.psf 

563 psfAvgPos = psf.getAveragePosition() 

564 psfSize = getPsfFwhm(science.psf) 

565 psfImg = psf.computeKernelImage(psfAvgPos) 

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

567 templateBorderSize=20, doApplyCalibration=True) 

568 

569 # Generate a random aperture correction map 

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

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

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

573 science.info.setApCorrMap(apCorrMap) 

574 

575 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

576 config.mode = "convolveTemplate" 

577 

578 def _run_and_check_images(doDecorrelation): 

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

580 """ 

581 config.doDecorrelation = doDecorrelation 

582 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

584 psfOut = output.difference.psf 

585 psfAvgPos = psfOut.getAveragePosition() 

586 if doDecorrelation: 

587 # Decorrelation requires recalculating the PSF, 

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

589 psfOutSize = getPsfFwhm(science.psf) 

590 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

591 else: 

592 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

593 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

594 

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

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

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

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

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

600 _run_and_check_images(doDecorrelation=True) 

601 _run_and_check_images(doDecorrelation=False) 

602 

603 def test_exposure_properties_convolve_science(self): 

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

605 when the science image is convolved. 

606 """ 

607 noiseLevel = 1. 

608 seed = 37 

609 rng = np.random.RandomState(seed) 

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

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

612 templateBorderSize=20, doApplyCalibration=True) 

613 psf = template.psf 

614 psfAvgPos = psf.getAveragePosition() 

615 psfSize = getPsfFwhm(template.psf) 

616 psfImg = psf.computeKernelImage(psfAvgPos) 

617 

618 # Generate a random aperture correction map 

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

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

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

622 science.info.setApCorrMap(apCorrMap) 

623 

624 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

625 config.mode = "convolveScience" 

626 

627 def _run_and_check_images(doDecorrelation): 

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

629 """ 

630 config.doDecorrelation = doDecorrelation 

631 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

633 if doDecorrelation: 

634 # Decorrelation requires recalculating the PSF, 

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

636 psfOutSize = getPsfFwhm(template.psf) 

637 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

638 else: 

639 psfOut = output.difference.psf 

640 psfAvgPos = psfOut.getAveragePosition() 

641 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

642 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

643 

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

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

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

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

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

649 

650 _run_and_check_images(doDecorrelation=True) 

651 _run_and_check_images(doDecorrelation=False) 

652 

653 def _compare_apCorrMaps(self, a, b): 

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

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

656 

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

658 

659 Parameters 

660 ---------- 

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

662 The two aperture correction maps to compare. 

663 """ 

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

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

666 value2 = b.get(name) 

667 self.assertIsNotNone(value2) 

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

669 self.assertFloatsAlmostEqual( 

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

671 

672 

673def setup_module(module): 

674 lsst.utils.tests.init() 

675 

676 

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

678 pass 

679 

680 

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

682 lsst.utils.tests.init() 

683 unittest.main()