Coverage for tests/test_subtractTask.py: 7%

605 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-08 08:40 +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 

29from lsst.ip.diffim import subtractImages 

30from lsst.pex.config import FieldValidationError 

31import lsst.utils.tests 

32import numpy as np 

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

34 evaluateMeanPsfFwhm, getPsfFwhm, makeStats, makeTestImage) 

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

72 noiseLevel = 1. 

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

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

75 templateBorderSize=20, doApplyCalibration=True) 

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

77 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

78 config.doSubtractBackground = False 

79 config.mode = "convolveTemplate" 

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

81 mask = template.mask 

82 x0 = 50 

83 x1 = 75 

84 y0 = 150 

85 y1 = 175 

86 scienceMaskCheck = {} 

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

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

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

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

91 

92 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

96 if maskPlane in diffimEmptyMaskPlanes: 

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

98 elif maskPlane in config.preserveTemplateMask: 

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

100 else: 

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

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

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

104 diffimMask = output.difference.mask 

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

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

107 if maskPlane in diffimEmptyMaskPlanes: 

108 self.assertEqual(diffimSum, 0) 

109 else: 

110 self.assertTrue(diffimSum >= scienceSum) 

111 

112 def test_equal_images(self): 

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

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

115 """ 

116 noiseLevel = 1. 

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

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

119 templateBorderSize=20, doApplyCalibration=True) 

120 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

121 config.doSubtractBackground = False 

122 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

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

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

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

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

134 makeStats(), statistic=afwMath.STDEV) 

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

136 

137 def test_psf_size(self): 

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

139 fwhmExposureBuffer and fwhmExposureGrid parameters are set. 

140 """ 

141 noiseLevel = 1. 

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

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

144 templateBorderSize=20, doApplyCalibration=True) 

145 

146 schema = afwTable.ExposureTable.makeMinimalSchema() 

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

148 exposureCatalog = afwTable.ExposureCatalog(schema) 

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

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

151 

152 record = exposureCatalog.addNew() 

153 record.setPsf(psf) 

154 record.setWcs(template.wcs) 

155 record.setD(weightKey, 1.0) 

156 record.setBBox(template.getBBox()) 

157 

158 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs) 

159 template.setPsf(customPsf) 

160 

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

162 with self.assertRaises(InvalidParameterError): 

163 getPsfFwhm(template.psf, True) 

164 

165 with self.assertRaises(InvalidParameterError): 

166 getPsfFwhm(template.psf, False) 

167 

168 # Test that evaluateMeanPsfFwhm runs successfully on the template. 

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

170 

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

172 # all points in the science image. 

173 fwhm1 = getPsfFwhm(science.psf, False) 

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

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

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

177 

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

179 fwhmExposureGrid=10), 

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

181 ) 

182 

183 # Test that the image subtraction task runs successfully. 

184 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

185 config.doSubtractBackground = False 

186 task = subtractImages.AlardLuptonSubtractTask(config=config) 

187 

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

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

190 task.run(template, science, sources) 

191 

192 # Check that evaluateMeanPsfFwhm was called. 

193 # This tests that getPsfFwhm failed raising InvalidParameterError, 

194 # that is caught and handled appropriately. 

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

196 "Evaluting PSF on a grid of points." 

197 ) 

198 self.assertIn(logMessage, cm.output) 

199 

200 def test_auto_convolveTemplate(self): 

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

202 the template psf is the smaller. 

203 """ 

204 noiseLevel = 1. 

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

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

207 templateBorderSize=20, doApplyCalibration=True) 

208 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

209 config.doSubtractBackground = False 

210 config.mode = "convolveTemplate" 

211 

212 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

214 

215 config.mode = "auto" 

216 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

219 

220 def test_auto_convolveScience(self): 

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

222 the science psf is the smaller. 

223 """ 

224 noiseLevel = 1. 

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

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

227 templateBorderSize=20, doApplyCalibration=True) 

228 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

229 config.doSubtractBackground = False 

230 config.mode = "convolveScience" 

231 

232 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

234 

235 config.mode = "auto" 

236 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

239 

240 def test_science_better(self): 

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

242 with the science psf being smaller than the template. 

243 """ 

244 statsCtrl = makeStats() 

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

246 

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

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

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

250 templateBorderSize=20, doApplyCalibration=True) 

251 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

252 config.doSubtractBackground = False 

253 config.mode = "convolveScience" 

254 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

262 statsCtrlDetect) 

263 

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

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

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

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

268 statsCtrl) 

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

270 statsCtrl, statistic=afwMath.STDEV) 

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

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

273 

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

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

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

277 

278 def test_template_better(self): 

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

280 with the template psf being smaller than the science. 

281 """ 

282 statsCtrl = makeStats() 

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

284 

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

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

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

288 templateBorderSize=20, doApplyCalibration=True) 

289 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

290 config.doSubtractBackground = False 

291 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

299 

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

301 statsCtrlDetect) 

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

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

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

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

306 statsCtrl) 

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

308 statsCtrl, statistic=afwMath.STDEV) 

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

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

311 

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

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

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

315 

316 def test_symmetry(self): 

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

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

319 should be nearly the same. 

320 """ 

321 noiseLevel = 1. 

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

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

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

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

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

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

328 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

329 config.mode = 'auto' 

330 config.doSubtractBackground = False 

331 task = subtractImages.AlardLuptonSubtractTask(config=config) 

332 

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

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

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

336 

337 delta = template_better.difference.clone() 

338 delta.image -= science_better.difference.image 

339 delta.variance -= science_better.difference.variance 

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

341 

342 statsCtrl = makeStats() 

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

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

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

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

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

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

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

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

351 

352 def test_few_sources(self): 

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

354 """ 

355 xSize = 256 

356 ySize = 256 

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

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

359 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

360 task = subtractImages.AlardLuptonSubtractTask(config=config) 

361 sources = sources[0:1] 

362 with self.assertRaisesRegex(RuntimeError, 

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

364 task.run(template, science, sources) 

365 

366 def test_order_equal_images(self): 

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

368 if the images are equivalent. 

369 """ 

370 noiseLevel = .1 

371 seed1 = 6 

372 seed2 = 7 

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

374 clearEdgeMask=True) 

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

376 templateBorderSize=0, doApplyCalibration=True, 

377 clearEdgeMask=True) 

378 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

379 config1.mode = "convolveTemplate" 

380 config1.doSubtractBackground = False 

381 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

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

383 

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

385 clearEdgeMask=True) 

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

387 templateBorderSize=0, doApplyCalibration=True, 

388 clearEdgeMask=True) 

389 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

390 config2.mode = "convolveScience" 

391 config2.doSubtractBackground = False 

392 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

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

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

395 results_convolveScience.difference.getBBox()) 

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

397 diff1 -= template1.maskedImage[bbox] 

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

399 diff2 -= template2.maskedImage[bbox] 

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

401 diff1.image.array, 

402 atol=noiseLevel*5.) 

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

404 diff2.image.array, 

405 atol=noiseLevel*5.) 

406 diffErr = noiseLevel*2 

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

408 results_convolveScience.difference[bbox].maskedImage, 

409 atol=diffErr*5.) 

410 

411 def test_background_subtraction(self): 

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

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

414 """ 

415 noiseLevel = 1. 

416 xSize = 512 

417 ySize = 512 

418 x0 = 123 

419 y0 = 456 

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

421 templateBorderSize=20, 

422 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

423 doApplyCalibration=True) 

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

425 

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

427 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

429 background=background_model, 

430 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

431 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

432 config.doSubtractBackground = True 

433 

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

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

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

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

438 statsCtrl = makeStats() 

439 

440 def _run_and_check_images(config, statsCtrl, mode): 

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

442 """ 

443 config.mode = mode 

444 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

446 

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

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

449 

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

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

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

453 

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

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

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

457 statsCtrl, statistic=afwMath.STDEV) 

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

459 

460 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

461 _run_and_check_images(config, statsCtrl, "convolveScience") 

462 

463 def test_scale_variance_convolve_template(self): 

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

465 """ 

466 scienceNoiseLevel = 4. 

467 templateNoiseLevel = 2. 

468 scaleFactor = 1.345 

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

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

471 

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

473 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

476 """ 

477 

478 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

479 config.doSubtractBackground = False 

480 config.doDecorrelation = doDecorrelation 

481 config.doScaleVariance = doScaleVariance 

482 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

484 if doScaleVariance: 

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

486 scaleFactor, atol=0.05) 

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

488 scaleFactor, atol=0.05) 

489 

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

491 if doDecorrelation: 

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

493 else: 

494 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

495 output.matchedTemplate.mask, 

496 statsCtrl) 

497 

498 if doScaleVariance: 

499 templateNoise *= scaleFactor 

500 scienceNoise *= scaleFactor 

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

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

503 

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

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

506 templateBorderSize=20, doApplyCalibration=True) 

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

508 # when the template and science variance planes are correct 

509 _run_and_check_images(science, template, sources, statsCtrl, 

510 doDecorrelation=True, doScaleVariance=True) 

511 _run_and_check_images(science, template, sources, statsCtrl, 

512 doDecorrelation=True, doScaleVariance=False) 

513 _run_and_check_images(science, template, sources, statsCtrl, 

514 doDecorrelation=False, doScaleVariance=True) 

515 _run_and_check_images(science, template, sources, statsCtrl, 

516 doDecorrelation=False, doScaleVariance=False) 

517 

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

519 # when the template variance plane is incorrect 

520 template.variance.array /= scaleFactor 

521 science.variance.array /= scaleFactor 

522 _run_and_check_images(science, template, sources, statsCtrl, 

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

524 _run_and_check_images(science, template, sources, statsCtrl, 

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

526 _run_and_check_images(science, template, sources, statsCtrl, 

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

528 _run_and_check_images(science, template, sources, statsCtrl, 

529 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

530 

531 def test_scale_variance_convolve_science(self): 

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

533 """ 

534 scienceNoiseLevel = 4. 

535 templateNoiseLevel = 2. 

536 scaleFactor = 1.345 

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

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

539 

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

541 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

544 """ 

545 

546 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

547 config.mode = "convolveScience" 

548 config.doSubtractBackground = False 

549 config.doDecorrelation = doDecorrelation 

550 config.doScaleVariance = doScaleVariance 

551 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

553 if doScaleVariance: 

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

555 scaleFactor, atol=0.05) 

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

557 scaleFactor, atol=0.05) 

558 

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

560 if doDecorrelation: 

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

562 else: 

563 scienceNoise = computeRobustStatistics(output.matchedScience.variance, 

564 output.matchedScience.mask, 

565 statsCtrl) 

566 

567 if doScaleVariance: 

568 templateNoise *= scaleFactor 

569 scienceNoise *= scaleFactor 

570 

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

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

573 

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

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

576 templateBorderSize=20, doApplyCalibration=True) 

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

578 # when the template and science variance planes are correct 

579 _run_and_check_images(science, template, sources, statsCtrl, 

580 doDecorrelation=True, doScaleVariance=True) 

581 _run_and_check_images(science, template, sources, statsCtrl, 

582 doDecorrelation=True, doScaleVariance=False) 

583 _run_and_check_images(science, template, sources, statsCtrl, 

584 doDecorrelation=False, doScaleVariance=True) 

585 _run_and_check_images(science, template, sources, statsCtrl, 

586 doDecorrelation=False, doScaleVariance=False) 

587 

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

589 # when the template and science variance planes are incorrect 

590 science.variance.array /= scaleFactor 

591 template.variance.array /= scaleFactor 

592 _run_and_check_images(science, template, sources, statsCtrl, 

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

594 _run_and_check_images(science, template, sources, statsCtrl, 

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

596 _run_and_check_images(science, template, sources, statsCtrl, 

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

598 _run_and_check_images(science, template, sources, statsCtrl, 

599 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

600 

601 def test_exposure_properties_convolve_template(self): 

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

603 when the template is convolved. 

604 """ 

605 noiseLevel = 1. 

606 seed = 37 

607 rng = np.random.RandomState(seed) 

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

609 psf = science.psf 

610 psfAvgPos = psf.getAveragePosition() 

611 psfSize = getPsfFwhm(science.psf) 

612 psfImg = psf.computeKernelImage(psfAvgPos) 

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

614 templateBorderSize=20, doApplyCalibration=True) 

615 

616 # Generate a random aperture correction map 

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

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

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

620 science.info.setApCorrMap(apCorrMap) 

621 

622 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

623 config.mode = "convolveTemplate" 

624 

625 def _run_and_check_images(doDecorrelation): 

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

627 """ 

628 config.doDecorrelation = doDecorrelation 

629 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

631 psfOut = output.difference.psf 

632 psfAvgPos = psfOut.getAveragePosition() 

633 if doDecorrelation: 

634 # Decorrelation requires recalculating the PSF, 

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

636 psfOutSize = getPsfFwhm(science.psf) 

637 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

638 else: 

639 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

640 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

641 

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

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

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

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

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

647 _run_and_check_images(doDecorrelation=True) 

648 _run_and_check_images(doDecorrelation=False) 

649 

650 def test_exposure_properties_convolve_science(self): 

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

652 when the science image is convolved. 

653 """ 

654 noiseLevel = 1. 

655 seed = 37 

656 rng = np.random.RandomState(seed) 

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

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

659 templateBorderSize=20, doApplyCalibration=True) 

660 psf = template.psf 

661 psfAvgPos = psf.getAveragePosition() 

662 psfSize = getPsfFwhm(template.psf) 

663 psfImg = psf.computeKernelImage(psfAvgPos) 

664 

665 # Generate a random aperture correction map 

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

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

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

669 science.info.setApCorrMap(apCorrMap) 

670 

671 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

672 config.mode = "convolveScience" 

673 

674 def _run_and_check_images(doDecorrelation): 

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

676 """ 

677 config.doDecorrelation = doDecorrelation 

678 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

680 if doDecorrelation: 

681 # Decorrelation requires recalculating the PSF, 

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

683 psfOutSize = getPsfFwhm(template.psf) 

684 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

685 else: 

686 psfOut = output.difference.psf 

687 psfAvgPos = psfOut.getAveragePosition() 

688 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

689 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

690 

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

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

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

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

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

696 

697 _run_and_check_images(doDecorrelation=True) 

698 _run_and_check_images(doDecorrelation=False) 

699 

700 def _compare_apCorrMaps(self, a, b): 

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

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

703 

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

705 

706 Parameters 

707 ---------- 

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

709 The two aperture correction maps to compare. 

710 """ 

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

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

713 value2 = b.get(name) 

714 self.assertIsNotNone(value2) 

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

716 self.assertFloatsAlmostEqual( 

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

718 

719 

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

721 

722 def test_mismatched_template(self): 

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

724 does not fully contain the science image. 

725 """ 

726 xSize = 200 

727 ySize = 200 

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

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

730 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

731 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

732 with self.assertRaises(AssertionError): 

733 task.run(template, science, sources) 

734 

735 def test_equal_images(self): 

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

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

738 """ 

739 noiseLevel = 1. 

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

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

742 templateBorderSize=20, doApplyCalibration=True) 

743 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

744 config.doSubtractBackground = False 

745 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

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

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

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

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

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

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

753 scoreMean = computeRobustStatistics(output.scoreExposure.image, 

754 output.scoreExposure.mask, 

755 statsCtrl) 

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

757 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

760 statsCtrl=statsCtrl, statistic=afwMath.STDEV) 

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

762 

763 def test_clear_template_mask(self): 

764 noiseLevel = 1. 

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

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

767 templateBorderSize=20, doApplyCalibration=True) 

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

769 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

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

771 mask = template.mask 

772 x0 = 50 

773 x1 = 75 

774 y0 = 150 

775 y1 = 175 

776 scienceMaskCheck = {} 

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

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

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

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

781 

782 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

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

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

786 if maskPlane in diffimEmptyMaskPlanes: 

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

788 elif maskPlane in config.preserveTemplateMask: 

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

790 else: 

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

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

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

794 diffimMask = output.scoreExposure.mask 

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

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

797 if maskPlane in diffimEmptyMaskPlanes: 

798 self.assertEqual(diffimSum, 0) 

799 else: 

800 self.assertTrue(diffimSum >= scienceSum) 

801 

802 def test_agnostic_template_psf(self): 

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

804 larger or smaller than the science image PSF. 

805 """ 

806 noiseLevel = .3 

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

808 noiseSeed=6, templateBorderSize=0) 

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

810 noiseSeed=7, doApplyCalibration=True) 

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

812 noiseSeed=8, doApplyCalibration=True) 

813 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

814 config.doSubtractBackground = False 

815 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

816 

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

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

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

820 

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

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

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

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

825 

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

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

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

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

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

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

832 statistic=afwMath.STDEV) 

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

834 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

837 

838 def test_few_sources(self): 

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

840 """ 

841 xSize = 256 

842 ySize = 256 

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

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

845 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

846 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

847 sources = sources[0:1] 

848 with self.assertRaisesRegex(RuntimeError, 

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

850 task.run(template, science, sources) 

851 

852 def test_background_subtraction(self): 

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

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

855 """ 

856 noiseLevel = 1. 

857 xSize = 512 

858 ySize = 512 

859 x0 = 123 

860 y0 = 456 

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

862 templateBorderSize=20, 

863 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

864 doApplyCalibration=True) 

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

866 

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

868 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

870 background=background_model, 

871 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

872 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

873 config.doSubtractBackground = True 

874 

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

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

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

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

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

880 

881 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

883 

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

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

886 

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

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

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

890 

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

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

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

894 statsCtrl, statistic=afwMath.STDEV) 

895 # get the img psf Noise Equivalent Area value 

896 nea = computePSFNoiseEquivalentArea(science.psf) 

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

898 

899 def test_scale_variance(self): 

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

901 """ 

902 scienceNoiseLevel = 4. 

903 templateNoiseLevel = 2. 

904 scaleFactor = 1.345 

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

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

907 

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

909 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

912 """ 

913 

914 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

915 config.doSubtractBackground = False 

916 config.doDecorrelation = doDecorrelation 

917 config.doScaleVariance = doScaleVariance 

918 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

920 if doScaleVariance: 

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

922 scaleFactor, atol=0.05) 

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

924 scaleFactor, atol=0.05) 

925 

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

927 # get the img psf Noise Equivalent Area value 

928 nea = computePSFNoiseEquivalentArea(science.psf) 

929 scienceNoise /= nea 

930 if doDecorrelation: 

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

932 templateNoise /= nea 

933 else: 

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

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

936 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

937 output.matchedTemplate.mask, 

938 statsCtrl) 

939 if doScaleVariance: 

940 templateNoise *= scaleFactor 

941 scienceNoise *= scaleFactor 

942 varMean = computeRobustStatistics(output.scoreExposure.variance, 

943 output.scoreExposure.mask, 

944 statsCtrl) 

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

946 

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

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

949 templateBorderSize=20, doApplyCalibration=True) 

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

951 # when the template and science variance planes are correct 

952 _run_and_check_images(science, template, sources, statsCtrl, 

953 doDecorrelation=True, doScaleVariance=True) 

954 _run_and_check_images(science, template, sources, statsCtrl, 

955 doDecorrelation=True, doScaleVariance=False) 

956 _run_and_check_images(science, template, sources, statsCtrl, 

957 doDecorrelation=False, doScaleVariance=True) 

958 _run_and_check_images(science, template, sources, statsCtrl, 

959 doDecorrelation=False, doScaleVariance=False) 

960 

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

962 # when the template variance plane is incorrect 

963 template.variance.array /= scaleFactor 

964 science.variance.array /= scaleFactor 

965 _run_and_check_images(science, template, sources, statsCtrl, 

966 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

967 _run_and_check_images(science, template, sources, statsCtrl, 

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

969 _run_and_check_images(science, template, sources, statsCtrl, 

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

971 _run_and_check_images(science, template, sources, statsCtrl, 

972 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

973 

974 def test_exposure_properties(self): 

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

976 with the Score image. 

977 """ 

978 noiseLevel = 1. 

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

980 psf = science.psf 

981 psfAvgPos = psf.getAveragePosition() 

982 psfSize = getPsfFwhm(science.psf) 

983 psfImg = psf.computeKernelImage(psfAvgPos) 

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

985 templateBorderSize=20, doApplyCalibration=True) 

986 

987 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

988 

989 def _run_and_check_images(doDecorrelation): 

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

991 """ 

992 config.doDecorrelation = doDecorrelation 

993 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

995 psfOut = output.scoreExposure.psf 

996 psfAvgPos = psfOut.getAveragePosition() 

997 if doDecorrelation: 

998 # Decorrelation requires recalculating the PSF, 

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

1000 psfOutSize = getPsfFwhm(science.psf) 

1001 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

1002 else: 

1003 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

1004 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

1005 

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

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

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

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

1010 _run_and_check_images(doDecorrelation=True) 

1011 _run_and_check_images(doDecorrelation=False) 

1012 

1013 

1014def setup_module(module): 

1015 lsst.utils.tests.init() 

1016 

1017 

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

1019 pass 

1020 

1021 

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

1023 lsst.utils.tests.init() 

1024 unittest.main()