Coverage for tests/test_subtractTask.py: 7%

641 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-02 11:35 +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.meas.algorithms as measAlg 

28from lsst.ip.diffim import subtractImages 

29from lsst.pex.config import FieldValidationError 

30import lsst.utils.tests 

31import numpy as np 

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

33 evaluateMeanPsfFwhm, getPsfFwhm, makeStats, makeTestImage) 

34from lsst.pex.exceptions import InvalidParameterError 

35 

36 

37class CustomCoaddPsf(measAlg.CoaddPsf): 

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

39 """ 

40 def getAveragePosition(self): 

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

42 

43 

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

45 

46 def test_allowed_config_modes(self): 

47 """Verify the allowable modes for convolution. 

48 """ 

49 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

50 config.mode = 'auto' 

51 config.mode = 'convolveScience' 

52 config.mode = 'convolveTemplate' 

53 

54 with self.assertRaises(FieldValidationError): 

55 config.mode = 'aotu' 

56 

57 def test_mismatched_template(self): 

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

59 does not fully contain the science image. 

60 """ 

61 xSize = 200 

62 ySize = 200 

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

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

65 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

66 task = subtractImages.AlardLuptonSubtractTask(config=config) 

67 with self.assertRaises(AssertionError): 

68 task.run(template, science, sources) 

69 

70 def test_clear_template_mask(self): 

71 noiseLevel = 1. 

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

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

74 templateBorderSize=20, doApplyCalibration=True) 

75 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"] 

76 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

77 config.doSubtractBackground = False 

78 config.mode = "convolveTemplate" 

79 # Ensure that each each mask plane is set for some pixels 

80 mask = template.mask 

81 x0 = 50 

82 x1 = 75 

83 y0 = 150 

84 y1 = 175 

85 scienceMaskCheck = {} 

86 for maskPlane in mask.getMaskPlaneDict().keys(): 

87 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0) 

88 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane) 

89 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0)) 

90 

91 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

93 # Verify that the template mask has been modified in place 

94 for maskPlane in mask.getMaskPlaneDict().keys(): 

95 if maskPlane in diffimEmptyMaskPlanes: 

96 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0)) 

97 elif maskPlane in config.preserveTemplateMask: 

98 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0)) 

99 else: 

100 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0)) 

101 # Mask planes set in the science image should also be set in the difference 

102 # Except the "DETECTED" planes should have been cleared 

103 diffimMask = output.difference.mask 

104 for maskPlane, scienceSum in scienceMaskCheck.items(): 

105 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0) 

106 if maskPlane in diffimEmptyMaskPlanes: 

107 self.assertEqual(diffimSum, 0) 

108 else: 

109 self.assertTrue(diffimSum >= scienceSum) 

110 

111 def test_equal_images(self): 

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

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

114 """ 

115 noiseLevel = 1. 

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

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

118 templateBorderSize=20, doApplyCalibration=True) 

119 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

120 config.doSubtractBackground = False 

121 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

128 statsCtrl = makeStats(badMaskPlanes=("EDGE", "BAD", "NO_DATA", "DETECTED", "DETECTED_NEGATIVE")) 

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

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

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

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

133 makeStats(), statistic=afwMath.STDEV) 

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

135 

136 def test_psf_size(self): 

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

138 fwhmExposureBuffer and fwhmExposureGrid parameters are set. 

139 """ 

140 noiseLevel = 1. 

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

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

143 templateBorderSize=20, doApplyCalibration=True) 

144 

145 schema = afwTable.ExposureTable.makeMinimalSchema() 

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

147 exposureCatalog = afwTable.ExposureCatalog(schema) 

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

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

150 

151 record = exposureCatalog.addNew() 

152 record.setPsf(psf) 

153 record.setWcs(template.wcs) 

154 record.setD(weightKey, 1.0) 

155 record.setBBox(template.getBBox()) 

156 

157 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs) 

158 template.setPsf(customPsf) 

159 

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

161 with self.assertRaises(InvalidParameterError): 

162 getPsfFwhm(template.psf, True) 

163 

164 with self.assertRaises(InvalidParameterError): 

165 getPsfFwhm(template.psf, False) 

166 

167 # Test that evaluateMeanPsfFwhm runs successfully on the template. 

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

169 

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

171 # all points in the science image. 

172 fwhm1 = getPsfFwhm(science.psf, False) 

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

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

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

176 

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

178 fwhmExposureGrid=10), 

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

180 ) 

181 

182 # Test that the image subtraction task runs successfully. 

183 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

184 config.doSubtractBackground = False 

185 task = subtractImages.AlardLuptonSubtractTask(config=config) 

186 

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

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

189 task.run(template, science, sources) 

190 

191 # Check that evaluateMeanPsfFwhm was called. 

192 # This tests that getPsfFwhm failed raising InvalidParameterError, 

193 # that is caught and handled appropriately. 

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

195 "Evaluting PSF on a grid of points." 

196 ) 

197 self.assertIn(logMessage, cm.output) 

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 

211 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

213 

214 config.mode = "auto" 

215 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

218 

219 def test_auto_convolveScience(self): 

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

221 the science psf is the smaller. 

222 """ 

223 noiseLevel = 1. 

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

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

226 templateBorderSize=20, doApplyCalibration=True) 

227 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

228 config.doSubtractBackground = False 

229 config.mode = "convolveScience" 

230 

231 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

233 

234 config.mode = "auto" 

235 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

238 

239 def test_science_better(self): 

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

241 with the science psf being smaller than the template. 

242 """ 

243 statsCtrl = makeStats() 

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

245 

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

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

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

249 templateBorderSize=20, doApplyCalibration=True) 

250 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

251 config.doSubtractBackground = False 

252 config.mode = "convolveScience" 

253 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

261 statsCtrlDetect) 

262 

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

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

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

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

267 statsCtrl) 

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

269 statsCtrl, statistic=afwMath.STDEV) 

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

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

272 

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

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

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

276 

277 def test_template_better(self): 

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

279 with the template psf being smaller than the science. 

280 """ 

281 statsCtrl = makeStats() 

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

283 

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

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

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

287 templateBorderSize=20, doApplyCalibration=True) 

288 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

289 config.doSubtractBackground = False 

290 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

298 

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

300 statsCtrlDetect) 

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

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

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

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

305 statsCtrl) 

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

307 statsCtrl, statistic=afwMath.STDEV) 

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

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

310 

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

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

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

314 

315 def test_symmetry(self): 

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

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

318 should be nearly the same. 

319 """ 

320 noiseLevel = 1. 

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

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

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

324 noiseSeed=6, templateBorderSize=0, doApplyCalibration=True) 

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

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

327 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

328 config.mode = 'auto' 

329 config.doSubtractBackground = False 

330 task = subtractImages.AlardLuptonSubtractTask(config=config) 

331 

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

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

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

335 

336 delta = template_better.difference.clone() 

337 delta.image -= science_better.difference.image 

338 delta.variance -= science_better.difference.variance 

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

340 

341 statsCtrl = makeStats() 

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

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

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

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

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

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

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

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

350 

351 def test_few_sources(self): 

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

353 """ 

354 xSize = 256 

355 ySize = 256 

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

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

358 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

359 task = subtractImages.AlardLuptonSubtractTask(config=config) 

360 sources = sources[0:1] 

361 with self.assertRaisesRegex(RuntimeError, 

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

363 task.run(template, science, sources) 

364 

365 def test_order_equal_images(self): 

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

367 if the images are equivalent. 

368 """ 

369 noiseLevel = .1 

370 seed1 = 6 

371 seed2 = 7 

372 science1, sources1 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1, 

373 clearEdgeMask=True) 

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

375 templateBorderSize=0, doApplyCalibration=True, 

376 clearEdgeMask=True) 

377 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

378 config1.mode = "convolveTemplate" 

379 config1.doSubtractBackground = False 

380 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

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

382 

383 science2, sources2 = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=seed1, 

384 clearEdgeMask=True) 

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

386 templateBorderSize=0, doApplyCalibration=True, 

387 clearEdgeMask=True) 

388 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

389 config2.mode = "convolveScience" 

390 config2.doSubtractBackground = False 

391 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

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

393 bbox = results_convolveTemplate.difference.getBBox().clippedTo( 

394 results_convolveScience.difference.getBBox()) 

395 diff1 = science1.maskedImage.clone()[bbox] 

396 diff1 -= template1.maskedImage[bbox] 

397 diff2 = science2.maskedImage.clone()[bbox] 

398 diff2 -= template2.maskedImage[bbox] 

399 self.assertFloatsAlmostEqual(results_convolveTemplate.difference[bbox].image.array, 

400 diff1.image.array, 

401 atol=noiseLevel*5.) 

402 self.assertFloatsAlmostEqual(results_convolveScience.difference[bbox].image.array, 

403 diff2.image.array, 

404 atol=noiseLevel*5.) 

405 diffErr = noiseLevel*2 

406 self.assertMaskedImagesAlmostEqual(results_convolveTemplate.difference[bbox].maskedImage, 

407 results_convolveScience.difference[bbox].maskedImage, 

408 atol=diffErr*5.) 

409 

410 def test_background_subtraction(self): 

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

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

413 """ 

414 noiseLevel = 1. 

415 xSize = 512 

416 ySize = 512 

417 x0 = 123 

418 y0 = 456 

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

420 templateBorderSize=20, 

421 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

422 doApplyCalibration=True) 

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

424 

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

426 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

428 background=background_model, 

429 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

430 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

431 config.doSubtractBackground = True 

432 

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

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

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

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

437 statsCtrl = makeStats() 

438 

439 def _run_and_check_images(config, statsCtrl, mode): 

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

441 """ 

442 config.mode = mode 

443 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

445 

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

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

448 

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

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

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

452 

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

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

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

456 statsCtrl, statistic=afwMath.STDEV) 

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

458 

459 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

460 _run_and_check_images(config, statsCtrl, "convolveScience") 

461 

462 def test_scale_variance_convolve_template(self): 

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

464 """ 

465 scienceNoiseLevel = 4. 

466 templateNoiseLevel = 2. 

467 scaleFactor = 1.345 

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

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

470 

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

472 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

475 """ 

476 

477 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

478 config.doSubtractBackground = False 

479 config.doDecorrelation = doDecorrelation 

480 config.doScaleVariance = doScaleVariance 

481 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

483 if doScaleVariance: 

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

485 scaleFactor, atol=0.05) 

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

487 scaleFactor, atol=0.05) 

488 

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

490 if doDecorrelation: 

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

492 else: 

493 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

494 output.matchedTemplate.mask, 

495 statsCtrl) 

496 

497 if doScaleVariance: 

498 templateNoise *= scaleFactor 

499 scienceNoise *= scaleFactor 

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

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

502 

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

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

505 templateBorderSize=20, doApplyCalibration=True) 

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

507 # when the template and science variance planes are correct 

508 _run_and_check_images(science, template, sources, statsCtrl, 

509 doDecorrelation=True, doScaleVariance=True) 

510 _run_and_check_images(science, template, sources, statsCtrl, 

511 doDecorrelation=True, doScaleVariance=False) 

512 _run_and_check_images(science, template, sources, statsCtrl, 

513 doDecorrelation=False, doScaleVariance=True) 

514 _run_and_check_images(science, template, sources, statsCtrl, 

515 doDecorrelation=False, doScaleVariance=False) 

516 

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

518 # when the template variance plane is incorrect 

519 template.variance.array /= scaleFactor 

520 science.variance.array /= scaleFactor 

521 _run_and_check_images(science, template, sources, statsCtrl, 

522 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

523 _run_and_check_images(science, template, sources, statsCtrl, 

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

525 _run_and_check_images(science, template, sources, statsCtrl, 

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

527 _run_and_check_images(science, template, sources, statsCtrl, 

528 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

529 

530 def test_scale_variance_convolve_science(self): 

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

532 """ 

533 scienceNoiseLevel = 4. 

534 templateNoiseLevel = 2. 

535 scaleFactor = 1.345 

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

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

538 

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

540 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

543 """ 

544 

545 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

546 config.mode = "convolveScience" 

547 config.doSubtractBackground = False 

548 config.doDecorrelation = doDecorrelation 

549 config.doScaleVariance = doScaleVariance 

550 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

552 if doScaleVariance: 

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

554 scaleFactor, atol=0.05) 

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

556 scaleFactor, atol=0.05) 

557 

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

559 if doDecorrelation: 

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

561 else: 

562 scienceNoise = computeRobustStatistics(output.matchedScience.variance, 

563 output.matchedScience.mask, 

564 statsCtrl) 

565 

566 if doScaleVariance: 

567 templateNoise *= scaleFactor 

568 scienceNoise *= scaleFactor 

569 

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

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

572 

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

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

575 templateBorderSize=20, doApplyCalibration=True) 

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

577 # when the template and science variance planes are correct 

578 _run_and_check_images(science, template, sources, statsCtrl, 

579 doDecorrelation=True, doScaleVariance=True) 

580 _run_and_check_images(science, template, sources, statsCtrl, 

581 doDecorrelation=True, doScaleVariance=False) 

582 _run_and_check_images(science, template, sources, statsCtrl, 

583 doDecorrelation=False, doScaleVariance=True) 

584 _run_and_check_images(science, template, sources, statsCtrl, 

585 doDecorrelation=False, doScaleVariance=False) 

586 

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

588 # when the template and science variance planes are incorrect 

589 science.variance.array /= scaleFactor 

590 template.variance.array /= scaleFactor 

591 _run_and_check_images(science, template, sources, statsCtrl, 

592 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

593 _run_and_check_images(science, template, sources, statsCtrl, 

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

595 _run_and_check_images(science, template, sources, statsCtrl, 

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

597 _run_and_check_images(science, template, sources, statsCtrl, 

598 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

599 

600 def test_exposure_properties_convolve_template(self): 

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

602 when the template is convolved. 

603 """ 

604 noiseLevel = 1. 

605 seed = 37 

606 rng = np.random.RandomState(seed) 

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

608 psf = science.psf 

609 psfAvgPos = psf.getAveragePosition() 

610 psfSize = getPsfFwhm(science.psf) 

611 psfImg = psf.computeKernelImage(psfAvgPos) 

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

613 templateBorderSize=20, doApplyCalibration=True) 

614 

615 # Generate a random aperture correction map 

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

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

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

619 science.info.setApCorrMap(apCorrMap) 

620 

621 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

622 config.mode = "convolveTemplate" 

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 

673 def _run_and_check_images(doDecorrelation): 

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

675 """ 

676 config.doDecorrelation = doDecorrelation 

677 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

679 if doDecorrelation: 

680 # Decorrelation requires recalculating the PSF, 

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

682 psfOutSize = getPsfFwhm(template.psf) 

683 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

684 else: 

685 psfOut = output.difference.psf 

686 psfAvgPos = psfOut.getAveragePosition() 

687 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

688 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

689 

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

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

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

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

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

695 

696 _run_and_check_images(doDecorrelation=True) 

697 _run_and_check_images(doDecorrelation=False) 

698 

699 def _compare_apCorrMaps(self, a, b): 

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

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

702 

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

704 

705 Parameters 

706 ---------- 

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

708 The two aperture correction maps to compare. 

709 """ 

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

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

712 value2 = b.get(name) 

713 self.assertIsNotNone(value2) 

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

715 self.assertFloatsAlmostEqual( 

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

717 

718 def test_fake_mask_plane_propagation(self): 

719 """Test that we have the mask planes related to fakes in diffim images. 

720 This is testing method called updateMasks 

721 """ 

722 xSize = 200 

723 ySize = 200 

724 science, sources = makeTestImage(psfSize=2.4, xSize=xSize, ySize=ySize) 

725 science_fake_img, science_fake_sources = makeTestImage( 

726 psfSize=2.4, xSize=xSize, ySize=ySize, seed=7, nSrc=2, noiseLevel=0.25, fluxRange=1 

727 ) 

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

729 tmplt_fake_img, tmplt_fake_sources = makeTestImage( 

730 psfSize=2.4, xSize=xSize, ySize=ySize, seed=9, nSrc=2, noiseLevel=0.25, fluxRange=1 

731 ) 

732 # created fakes and added them to the images 

733 science.image.array += science_fake_img.image.array 

734 template.image.array += tmplt_fake_img.image.array 

735 

736 # TODO: DM-40796 update to INJECTED names when source injection gets refactored 

737 # adding mask planes to both science and template images 

738 science_mask_planes = science.mask.addMaskPlane("FAKE") 

739 template_mask_planes = template.mask.addMaskPlane("FAKE") 

740 

741 for a_science_source in science_fake_sources: 

742 # 3 x 3 masking of the source locations is fine 

743 bbox = lsst.geom.Box2I( 

744 lsst.geom.Point2I(a_science_source.getX(), a_science_source.getY()), lsst.geom.Extent2I(3, 3) 

745 ) 

746 science[bbox].mask.array |= science_mask_planes 

747 

748 for a_template_source in tmplt_fake_sources: 

749 # 3 x 3 masking of the source locations is fine 

750 bbox = lsst.geom.Box2I( 

751 lsst.geom.Point2I(a_template_source.getX(), a_template_source.getY()), 

752 lsst.geom.Extent2I(3, 3) 

753 ) 

754 template[bbox].mask.array |= template_mask_planes 

755 

756 science_fake_masked = (science.mask.array & science.mask.getPlaneBitMask("FAKE")) > 0 

757 template_fake_masked = (template.mask.array & template.mask.getPlaneBitMask("FAKE")) > 0 

758 

759 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

760 task = subtractImages.AlardLuptonSubtractTask(config=config) 

761 subtraction = task.run(template, science, sources) 

762 

763 # check subtraction mask plane is set where we set the previous masks 

764 diff_mask = subtraction.difference.mask 

765 

766 # science mask should be now in INJECTED 

767 inj_masked = (diff_mask.array & diff_mask.getPlaneBitMask("INJECTED")) > 0 

768 

769 # template mask should be now in INJECTED_TEMPLATE 

770 injTmplt_masked = (diff_mask.array & diff_mask.getPlaneBitMask("INJECTED_TEMPLATE")) > 0 

771 

772 self.assertEqual(np.sum(inj_masked.astype(int)-science_fake_masked.astype(int)), 0) 

773 self.assertEqual(np.sum(injTmplt_masked.astype(int)-template_fake_masked.astype(int)), 0) 

774 

775 

776class AlardLuptonPreconvolveSubtractTest(lsst.utils.tests.TestCase): 

777 

778 def test_mismatched_template(self): 

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

780 does not fully contain the science image. 

781 """ 

782 xSize = 200 

783 ySize = 200 

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

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

786 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

787 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

788 with self.assertRaises(AssertionError): 

789 task.run(template, science, sources) 

790 

791 def test_equal_images(self): 

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

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

794 """ 

795 noiseLevel = 1. 

796 xSize = 400 

797 ySize = 400 

798 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6, 

799 xSize=xSize, ySize=ySize) 

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

801 templateBorderSize=20, doApplyCalibration=True, 

802 xSize=xSize, ySize=ySize) 

803 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

804 config.doSubtractBackground = False 

805 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

807 # There shoud be no NaN values in the Score image 

808 self.assertTrue(np.all(np.isfinite(output.scoreExposure.image.array))) 

809 # Mean of Score image should be close to zero. 

810 meanError = noiseLevel/np.sqrt(output.scoreExposure.image.array.size) 

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

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

813 scoreMean = computeRobustStatistics(output.scoreExposure.image, 

814 output.scoreExposure.mask, 

815 statsCtrl) 

816 self.assertFloatsAlmostEqual(scoreMean, 0, atol=5*meanError) 

817 nea = computePSFNoiseEquivalentArea(science.psf) 

818 # stddev of Score image should be close to expected value. 

819 scoreStd = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask, 

820 statsCtrl=statsCtrl, statistic=afwMath.STDEV) 

821 self.assertFloatsAlmostEqual(scoreStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1) 

822 

823 def test_clear_template_mask(self): 

824 noiseLevel = 1. 

825 xSize = 400 

826 ySize = 400 

827 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, 

828 xSize=xSize, ySize=ySize) 

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

830 templateBorderSize=20, doApplyCalibration=True, 

831 xSize=xSize, ySize=ySize) 

832 diffimEmptyMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE"] 

833 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

834 config.doSubtractBackground = False # Ensure that each each mask plane is set for some pixels 

835 mask = template.mask 

836 x0 = 50 

837 x1 = 75 

838 y0 = 150 

839 y1 = 175 

840 scienceMaskCheck = {} 

841 for maskPlane in mask.getMaskPlaneDict().keys(): 

842 scienceMaskCheck[maskPlane] = np.sum(science.mask.array & mask.getPlaneBitMask(maskPlane) > 0) 

843 mask.array[x0: x1, y0: y1] |= mask.getPlaneBitMask(maskPlane) 

844 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0)) 

845 

846 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

848 # Verify that the template mask has been modified in place 

849 for maskPlane in mask.getMaskPlaneDict().keys(): 

850 if maskPlane in diffimEmptyMaskPlanes: 

851 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0)) 

852 elif maskPlane in config.preserveTemplateMask: 

853 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) > 0)) 

854 else: 

855 self.assertTrue(np.sum(mask.array & mask.getPlaneBitMask(maskPlane) == 0)) 

856 # Mask planes set in the science image should also be set in the difference 

857 # Except the "DETECTED" planes should have been cleared 

858 diffimMask = output.scoreExposure.mask 

859 for maskPlane, scienceSum in scienceMaskCheck.items(): 

860 diffimSum = np.sum(diffimMask.array & mask.getPlaneBitMask(maskPlane) > 0) 

861 if maskPlane in diffimEmptyMaskPlanes: 

862 self.assertEqual(diffimSum, 0) 

863 else: 

864 self.assertTrue(diffimSum >= scienceSum) 

865 

866 def test_agnostic_template_psf(self): 

867 """Test that the Score image is the same whether the template PSF is 

868 larger or smaller than the science image PSF. 

869 """ 

870 noiseLevel = .3 

871 xSize = 400 

872 ySize = 400 

873 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, 

874 noiseSeed=6, templateBorderSize=0, 

875 xSize=xSize, ySize=ySize) 

876 template1, _ = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, 

877 noiseSeed=7, doApplyCalibration=True, 

878 xSize=xSize, ySize=ySize) 

879 template2, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, 

880 noiseSeed=8, doApplyCalibration=True, 

881 xSize=xSize, ySize=ySize) 

882 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

883 config.doSubtractBackground = False 

884 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

885 

886 science_better = task.run(template1, science.clone(), sources) 

887 template_better = task.run(template2, science, sources) 

888 bbox = science_better.scoreExposure.getBBox().clippedTo(template_better.scoreExposure.getBBox()) 

889 

890 delta = template_better.scoreExposure[bbox].clone() 

891 delta.image -= science_better.scoreExposure[bbox].image 

892 delta.variance -= science_better.scoreExposure[bbox].variance 

893 delta.mask.array &= science_better.scoreExposure[bbox].mask.array 

894 

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

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

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

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

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

900 deltaStd = computeRobustStatistics(delta.image, delta.mask, statsCtrl, 

901 statistic=afwMath.STDEV) 

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

903 nea = computePSFNoiseEquivalentArea(science.psf) 

904 # stddev of Score image should be close to expected value 

905 self.assertFloatsAlmostEqual(deltaStd, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=.1) 

906 

907 def test_few_sources(self): 

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

909 """ 

910 xSize = 256 

911 ySize = 256 

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

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

914 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

915 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

916 sources = sources[0:1] 

917 with self.assertRaisesRegex(RuntimeError, 

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

919 task.run(template, science, sources) 

920 

921 def test_background_subtraction(self): 

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

923 and that it is subtracted correctly in the Score image. 

924 """ 

925 noiseLevel = 1. 

926 xSize = 512 

927 ySize = 512 

928 x0 = 123 

929 y0 = 456 

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

931 templateBorderSize=20, 

932 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

933 doApplyCalibration=True) 

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

935 

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

937 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

939 background=background_model, 

940 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

941 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

942 config.doSubtractBackground = True 

943 

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

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

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

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

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

949 

950 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

952 

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

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

955 

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

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

958 np.array(params), rtol=0.2) 

959 

960 # stddev of Score image should be close to expected value. 

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

962 stdVal = computeRobustStatistics(output.scoreExposure.image, output.scoreExposure.mask, 

963 statsCtrl, statistic=afwMath.STDEV) 

964 # get the img psf Noise Equivalent Area value 

965 nea = computePSFNoiseEquivalentArea(science.psf) 

966 self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1) 

967 

968 def test_scale_variance(self): 

969 """Check variance scaling of the Score image. 

970 """ 

971 scienceNoiseLevel = 4. 

972 templateNoiseLevel = 2. 

973 scaleFactor = 1.345 

974 xSize = 400 

975 ySize = 400 

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

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

978 

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

980 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

983 """ 

984 

985 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

986 config.doSubtractBackground = False 

987 config.doDecorrelation = doDecorrelation 

988 config.doScaleVariance = doScaleVariance 

989 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

991 if doScaleVariance: 

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

993 scaleFactor, atol=0.05) 

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

995 scaleFactor, atol=0.05) 

996 

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

998 # get the img psf Noise Equivalent Area value 

999 nea = computePSFNoiseEquivalentArea(science.psf) 

1000 scienceNoise /= nea 

1001 if doDecorrelation: 

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

1003 templateNoise /= nea 

1004 else: 

1005 # Don't divide by NEA in this case, since the template is convolved 

1006 # and in the same units as the Score exposure. 

1007 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

1008 output.matchedTemplate.mask, 

1009 statsCtrl) 

1010 if doScaleVariance: 

1011 templateNoise *= scaleFactor 

1012 scienceNoise *= scaleFactor 

1013 varMean = computeRobustStatistics(output.scoreExposure.variance, 

1014 output.scoreExposure.mask, 

1015 statsCtrl) 

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

1017 

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

1019 xSize=xSize, ySize=ySize) 

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

1021 templateBorderSize=20, doApplyCalibration=True, 

1022 xSize=xSize, ySize=ySize) 

1023 # Verify that the variance plane of the Score image is correct 

1024 # when the template and science variance planes are correct 

1025 _run_and_check_images(science, template, sources, statsCtrl, 

1026 doDecorrelation=True, doScaleVariance=True) 

1027 _run_and_check_images(science, template, sources, statsCtrl, 

1028 doDecorrelation=True, doScaleVariance=False) 

1029 _run_and_check_images(science, template, sources, statsCtrl, 

1030 doDecorrelation=False, doScaleVariance=True) 

1031 _run_and_check_images(science, template, sources, statsCtrl, 

1032 doDecorrelation=False, doScaleVariance=False) 

1033 

1034 # Verify that the variance plane of the Score image is correct 

1035 # when the template variance plane is incorrect 

1036 template.variance.array /= scaleFactor 

1037 science.variance.array /= scaleFactor 

1038 _run_and_check_images(science, template, sources, statsCtrl, 

1039 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

1040 _run_and_check_images(science, template, sources, statsCtrl, 

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

1042 _run_and_check_images(science, template, sources, statsCtrl, 

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

1044 _run_and_check_images(science, template, sources, statsCtrl, 

1045 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

1046 

1047 def test_exposure_properties(self): 

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

1049 with the Score image. 

1050 """ 

1051 noiseLevel = 1. 

1052 xSize = 400 

1053 ySize = 400 

1054 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, 

1055 xSize=xSize, ySize=ySize) 

1056 psf = science.psf 

1057 psfAvgPos = psf.getAveragePosition() 

1058 psfSize = getPsfFwhm(science.psf) 

1059 psfImg = psf.computeKernelImage(psfAvgPos) 

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

1061 templateBorderSize=20, doApplyCalibration=True, 

1062 xSize=xSize, ySize=ySize) 

1063 

1064 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1065 

1066 def _run_and_check_images(doDecorrelation): 

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

1068 """ 

1069 config.doDecorrelation = doDecorrelation 

1070 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

1072 psfOut = output.scoreExposure.psf 

1073 psfAvgPos = psfOut.getAveragePosition() 

1074 if doDecorrelation: 

1075 # Decorrelation requires recalculating the PSF, 

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

1077 psfOutSize = getPsfFwhm(science.psf) 

1078 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

1079 else: 

1080 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

1081 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

1082 

1083 # check PSF, WCS, bbox, filterLabel, photoCalib 

1084 self.assertWcsAlmostEqualOverBBox(science.wcs, output.scoreExposure.wcs, science.getBBox()) 

1085 self.assertEqual(science.filter, output.scoreExposure.filter) 

1086 self.assertEqual(science.photoCalib, output.scoreExposure.photoCalib) 

1087 _run_and_check_images(doDecorrelation=True) 

1088 _run_and_check_images(doDecorrelation=False) 

1089 

1090 

1091def setup_module(module): 

1092 lsst.utils.tests.init() 

1093 

1094 

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

1096 pass 

1097 

1098 

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

1100 lsst.utils.tests.init() 

1101 unittest.main()