Coverage for tests/test_subtractTask.py: 6%

732 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-16 11:48 +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 

30from lsst.pipe.base import NoWorkFound 

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

72 noiseLevel = 1. 

73 border = 20 

74 xSize = 400 

75 ySize = 400 

76 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, nSrc=50, 

77 xSize=xSize, ySize=ySize) 

78 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, nSrc=50, 

79 templateBorderSize=border, doApplyCalibration=True, 

80 xSize=xSize, ySize=ySize) 

81 

82 science_height = science.getBBox().getDimensions().getY() 

83 

84 def _run_and_check_coverage(template_coverage, 

85 requiredTemplateFraction=0.1, 

86 minTemplateFractionForExpectedSuccess=0.2): 

87 template_cut = template.clone() 

88 template_height = int(science_height*template_coverage + border) 

89 template_cut.image.array[:, template_height:] = 0 

90 template_cut.mask.array[:, template_height:] = template_cut.mask.getPlaneBitMask('NO_DATA') 

91 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

92 config.requiredTemplateFraction = requiredTemplateFraction 

93 config.minTemplateFractionForExpectedSuccess = minTemplateFractionForExpectedSuccess 

94 if template_coverage < requiredTemplateFraction: 

95 doRaise = True 

96 elif template_coverage < minTemplateFractionForExpectedSuccess: 

97 doRaise = True 

98 else: 

99 doRaise = False 

100 task = subtractImages.AlardLuptonSubtractTask(config=config) 

101 if doRaise: 

102 with self.assertRaises(NoWorkFound): 

103 task.run(template_cut, science.clone(), sources.copy(deep=True)) 

104 else: 

105 task.run(template_cut, science.clone(), sources.copy(deep=True)) 

106 _run_and_check_coverage(template_coverage=0.09) 

107 _run_and_check_coverage(template_coverage=0.19) 

108 _run_and_check_coverage(template_coverage=0.7) 

109 

110 def test_clear_template_mask(self): 

111 noiseLevel = 1. 

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

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

114 templateBorderSize=20, doApplyCalibration=True) 

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

116 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

117 config.doSubtractBackground = False 

118 config.mode = "convolveTemplate" 

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

120 mask = template.mask 

121 x0 = 50 

122 x1 = 75 

123 y0 = 150 

124 y1 = 175 

125 scienceMaskCheck = {} 

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

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

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

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

130 

131 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

135 if maskPlane in diffimEmptyMaskPlanes: 

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

137 elif maskPlane in config.preserveTemplateMask: 

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

139 else: 

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

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

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

143 diffimMask = output.difference.mask 

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

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

146 if maskPlane in diffimEmptyMaskPlanes: 

147 self.assertEqual(diffimSum, 0) 

148 else: 

149 self.assertTrue(diffimSum >= scienceSum) 

150 

151 def test_equal_images(self): 

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

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

154 """ 

155 noiseLevel = 1. 

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

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

158 templateBorderSize=20, doApplyCalibration=True) 

159 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

160 config.doSubtractBackground = False 

161 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

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

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

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

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

173 makeStats(), statistic=afwMath.STDEV) 

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

175 

176 def test_psf_size(self): 

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

178 fwhmExposureBuffer and fwhmExposureGrid parameters are set. 

179 """ 

180 noiseLevel = 1. 

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

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

183 templateBorderSize=20, doApplyCalibration=True) 

184 

185 schema = afwTable.ExposureTable.makeMinimalSchema() 

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

187 exposureCatalog = afwTable.ExposureCatalog(schema) 

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

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

190 

191 record = exposureCatalog.addNew() 

192 record.setPsf(psf) 

193 record.setWcs(template.wcs) 

194 record.setD(weightKey, 1.0) 

195 record.setBBox(template.getBBox()) 

196 

197 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs) 

198 template.setPsf(customPsf) 

199 

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

201 with self.assertRaises(InvalidParameterError): 

202 getPsfFwhm(template.psf, True) 

203 

204 with self.assertRaises(InvalidParameterError): 

205 getPsfFwhm(template.psf, False) 

206 

207 # Test that evaluateMeanPsfFwhm runs successfully on the template. 

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

209 

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

211 # all points in the science image. 

212 fwhm1 = getPsfFwhm(science.psf, False) 

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

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

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

216 

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

218 fwhmExposureGrid=10), 

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

220 ) 

221 

222 # Test that the image subtraction task runs successfully. 

223 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

224 config.doSubtractBackground = False 

225 task = subtractImages.AlardLuptonSubtractTask(config=config) 

226 

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

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

229 task.run(template, science, sources) 

230 

231 # Check that evaluateMeanPsfFwhm was called. 

232 # This tests that getPsfFwhm failed raising InvalidParameterError, 

233 # that is caught and handled appropriately. 

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

235 "Evaluting PSF on a grid of points." 

236 ) 

237 self.assertIn(logMessage, cm.output) 

238 

239 def test_auto_convolveTemplate(self): 

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

241 the template psf is the smaller. 

242 """ 

243 noiseLevel = 1. 

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

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

246 templateBorderSize=20, doApplyCalibration=True) 

247 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

248 config.doSubtractBackground = False 

249 config.mode = "convolveTemplate" 

250 

251 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

253 

254 config.mode = "auto" 

255 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

258 

259 def test_auto_convolveScience(self): 

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

261 the science psf is the smaller. 

262 """ 

263 noiseLevel = 1. 

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

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

266 templateBorderSize=20, doApplyCalibration=True) 

267 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

268 config.doSubtractBackground = False 

269 config.mode = "convolveScience" 

270 

271 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

273 

274 config.mode = "auto" 

275 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

278 

279 def test_science_better(self): 

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

281 with the science psf being smaller than the template. 

282 """ 

283 statsCtrl = makeStats() 

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

285 

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

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

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

289 templateBorderSize=20, doApplyCalibration=True) 

290 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

291 config.doSubtractBackground = False 

292 config.mode = "convolveScience" 

293 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

301 statsCtrlDetect) 

302 

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

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

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

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

307 statsCtrl) 

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

309 statsCtrl, statistic=afwMath.STDEV) 

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

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

312 

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

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

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

316 

317 def test_template_better(self): 

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

319 with the template psf being smaller than the science. 

320 """ 

321 statsCtrl = makeStats() 

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

323 

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

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

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

327 templateBorderSize=20, doApplyCalibration=True) 

328 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

329 config.doSubtractBackground = False 

330 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

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

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

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

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

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

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

338 

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

340 statsCtrlDetect) 

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

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

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

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

345 statsCtrl) 

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

347 statsCtrl, statistic=afwMath.STDEV) 

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

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

350 

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

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

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

354 

355 def test_symmetry(self): 

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

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

358 should be nearly the same. 

359 """ 

360 noiseLevel = 1. 

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

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

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

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

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

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

367 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

368 config.mode = 'auto' 

369 config.doSubtractBackground = False 

370 task = subtractImages.AlardLuptonSubtractTask(config=config) 

371 

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

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

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

375 

376 delta = template_better.difference.clone() 

377 delta.image -= science_better.difference.image 

378 delta.variance -= science_better.difference.variance 

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

380 

381 statsCtrl = makeStats() 

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

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

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

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

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

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

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

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

390 

391 def test_few_sources(self): 

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

393 """ 

394 xSize = 256 

395 ySize = 256 

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

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

398 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

399 task = subtractImages.AlardLuptonSubtractTask(config=config) 

400 sources = sources[0:1] 

401 with self.assertRaisesRegex(RuntimeError, 

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

403 task.run(template, science, sources) 

404 

405 def test_kernel_source_selector(self): 

406 """Check that kernel source selection behaves as expected. 

407 """ 

408 xSize = 256 

409 ySize = 256 

410 nSourcesSimulated = 20 

411 science, sources = makeTestImage(psfSize=2.4, nSrc=nSourcesSimulated, 

412 xSize=xSize, ySize=ySize) 

413 template, _ = makeTestImage(psfSize=2.0, nSrc=nSourcesSimulated, 

414 xSize=xSize, ySize=ySize, doApplyCalibration=True) 

415 badSourceFlag = "slot_Centroid_flag" 

416 

417 def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3): 

418 sources = sourcesIn.copy(deep=True) 

419 # Verify that source flags are not set in the input catalog 

420 self.assertEqual(np.sum(sources[badSourceFlag]), 0) 

421 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

422 config.badSourceFlags = [badSourceFlag, ] 

423 config.maxKernelSources = maxKernelSources 

424 config.minKernelSources = minKernelSources 

425 

426 task = subtractImages.AlardLuptonSubtractTask(config=config) 

427 nSources = len(sources) 

428 # Flag a third of the sources 

429 sources[0:: 3][badSourceFlag] = True 

430 nBadSources = np.sum(sources[badSourceFlag]) 

431 if maxKernelSources > 0: 

432 nGoodSources = np.minimum(nSources - nBadSources, maxKernelSources) 

433 else: 

434 nGoodSources = nSources - nBadSources 

435 

436 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr() 

437 signalToNoise = signalToNoise[~sources[badSourceFlag]] 

438 signalToNoise.sort() 

439 selectSources = task._sourceSelector(sources, science.mask) 

440 self.assertEqual(nGoodSources, len(selectSources)) 

441 signalToNoiseOut = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr() 

442 signalToNoiseOut.sort() 

443 self.assertFloatsAlmostEqual(signalToNoise[-nGoodSources:], signalToNoiseOut) 

444 

445 _run_and_check_sources(sources) 

446 _run_and_check_sources(sources, maxKernelSources=len(sources)//3) 

447 _run_and_check_sources(sources, maxKernelSources=-1) 

448 with self.assertRaises(RuntimeError): 

449 _run_and_check_sources(sources, minKernelSources=1000) 

450 

451 def test_order_equal_images(self): 

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

453 if the images are equivalent. 

454 """ 

455 noiseLevel = .1 

456 seed1 = 6 

457 seed2 = 7 

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

459 clearEdgeMask=True) 

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

461 templateBorderSize=0, doApplyCalibration=True, 

462 clearEdgeMask=True) 

463 config1 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

464 config1.mode = "convolveTemplate" 

465 config1.doSubtractBackground = False 

466 task1 = subtractImages.AlardLuptonSubtractTask(config=config1) 

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

468 

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

470 clearEdgeMask=True) 

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

472 templateBorderSize=0, doApplyCalibration=True, 

473 clearEdgeMask=True) 

474 config2 = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

475 config2.mode = "convolveScience" 

476 config2.doSubtractBackground = False 

477 task2 = subtractImages.AlardLuptonSubtractTask(config=config2) 

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

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

480 results_convolveScience.difference.getBBox()) 

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

482 diff1 -= template1.maskedImage[bbox] 

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

484 diff2 -= template2.maskedImage[bbox] 

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

486 diff1.image.array, 

487 atol=noiseLevel*5.) 

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

489 diff2.image.array, 

490 atol=noiseLevel*5.) 

491 diffErr = noiseLevel*2 

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

493 results_convolveScience.difference[bbox].maskedImage, 

494 atol=diffErr*5.) 

495 

496 def test_background_subtraction(self): 

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

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

499 """ 

500 noiseLevel = 1. 

501 xSize = 512 

502 ySize = 512 

503 x0 = 123 

504 y0 = 456 

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

506 templateBorderSize=20, 

507 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

508 doApplyCalibration=True) 

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

510 

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

512 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

514 background=background_model, 

515 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

516 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

517 config.doSubtractBackground = True 

518 

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

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

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

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

523 statsCtrl = makeStats() 

524 

525 def _run_and_check_images(config, statsCtrl, mode): 

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

527 """ 

528 config.mode = mode 

529 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

531 

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

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

534 

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

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

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

538 

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

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

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

542 statsCtrl, statistic=afwMath.STDEV) 

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

544 

545 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

546 _run_and_check_images(config, statsCtrl, "convolveScience") 

547 

548 def test_scale_variance_convolve_template(self): 

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

550 """ 

551 scienceNoiseLevel = 4. 

552 templateNoiseLevel = 2. 

553 scaleFactor = 1.345 

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

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

556 

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

558 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

561 """ 

562 

563 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

564 config.doSubtractBackground = False 

565 config.doDecorrelation = doDecorrelation 

566 config.doScaleVariance = doScaleVariance 

567 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

569 if doScaleVariance: 

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

571 scaleFactor, atol=0.05) 

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

573 scaleFactor, atol=0.05) 

574 

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

576 if doDecorrelation: 

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

578 else: 

579 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

580 output.matchedTemplate.mask, 

581 statsCtrl) 

582 

583 if doScaleVariance: 

584 templateNoise *= scaleFactor 

585 scienceNoise *= scaleFactor 

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

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

588 

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

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

591 templateBorderSize=20, doApplyCalibration=True) 

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

593 # when the template and science variance planes are correct 

594 _run_and_check_images(science, template, sources, statsCtrl, 

595 doDecorrelation=True, doScaleVariance=True) 

596 _run_and_check_images(science, template, sources, statsCtrl, 

597 doDecorrelation=True, doScaleVariance=False) 

598 _run_and_check_images(science, template, sources, statsCtrl, 

599 doDecorrelation=False, doScaleVariance=True) 

600 _run_and_check_images(science, template, sources, statsCtrl, 

601 doDecorrelation=False, doScaleVariance=False) 

602 

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

604 # when the template variance plane is incorrect 

605 template.variance.array /= scaleFactor 

606 science.variance.array /= scaleFactor 

607 _run_and_check_images(science, template, sources, statsCtrl, 

608 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

609 _run_and_check_images(science, template, sources, statsCtrl, 

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

611 _run_and_check_images(science, template, sources, statsCtrl, 

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

613 _run_and_check_images(science, template, sources, statsCtrl, 

614 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

615 

616 def test_scale_variance_convolve_science(self): 

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

618 """ 

619 scienceNoiseLevel = 4. 

620 templateNoiseLevel = 2. 

621 scaleFactor = 1.345 

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

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

624 

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

626 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

629 """ 

630 

631 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

632 config.mode = "convolveScience" 

633 config.doSubtractBackground = False 

634 config.doDecorrelation = doDecorrelation 

635 config.doScaleVariance = doScaleVariance 

636 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

638 if doScaleVariance: 

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

640 scaleFactor, atol=0.05) 

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

642 scaleFactor, atol=0.05) 

643 

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

645 if doDecorrelation: 

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

647 else: 

648 scienceNoise = computeRobustStatistics(output.matchedScience.variance, 

649 output.matchedScience.mask, 

650 statsCtrl) 

651 

652 if doScaleVariance: 

653 templateNoise *= scaleFactor 

654 scienceNoise *= scaleFactor 

655 

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

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

658 

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

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

661 templateBorderSize=20, doApplyCalibration=True) 

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

663 # when the template and science variance planes are correct 

664 _run_and_check_images(science, template, sources, statsCtrl, 

665 doDecorrelation=True, doScaleVariance=True) 

666 _run_and_check_images(science, template, sources, statsCtrl, 

667 doDecorrelation=True, doScaleVariance=False) 

668 _run_and_check_images(science, template, sources, statsCtrl, 

669 doDecorrelation=False, doScaleVariance=True) 

670 _run_and_check_images(science, template, sources, statsCtrl, 

671 doDecorrelation=False, doScaleVariance=False) 

672 

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

674 # when the template and science variance planes are incorrect 

675 science.variance.array /= scaleFactor 

676 template.variance.array /= scaleFactor 

677 _run_and_check_images(science, template, sources, statsCtrl, 

678 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

679 _run_and_check_images(science, template, sources, statsCtrl, 

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

681 _run_and_check_images(science, template, sources, statsCtrl, 

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

683 _run_and_check_images(science, template, sources, statsCtrl, 

684 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

685 

686 def test_exposure_properties_convolve_template(self): 

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

688 when the template is convolved. 

689 """ 

690 noiseLevel = 1. 

691 seed = 37 

692 rng = np.random.RandomState(seed) 

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

694 psf = science.psf 

695 psfAvgPos = psf.getAveragePosition() 

696 psfSize = getPsfFwhm(science.psf) 

697 psfImg = psf.computeKernelImage(psfAvgPos) 

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

699 templateBorderSize=20, doApplyCalibration=True) 

700 

701 # Generate a random aperture correction map 

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

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

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

705 science.info.setApCorrMap(apCorrMap) 

706 

707 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

708 config.mode = "convolveTemplate" 

709 

710 def _run_and_check_images(doDecorrelation): 

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

712 """ 

713 config.doDecorrelation = doDecorrelation 

714 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

716 psfOut = output.difference.psf 

717 psfAvgPos = psfOut.getAveragePosition() 

718 if doDecorrelation: 

719 # Decorrelation requires recalculating the PSF, 

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

721 psfOutSize = getPsfFwhm(science.psf) 

722 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

723 else: 

724 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

725 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

726 

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

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

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

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

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

732 _run_and_check_images(doDecorrelation=True) 

733 _run_and_check_images(doDecorrelation=False) 

734 

735 def test_exposure_properties_convolve_science(self): 

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

737 when the science image is convolved. 

738 """ 

739 noiseLevel = 1. 

740 seed = 37 

741 rng = np.random.RandomState(seed) 

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

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

744 templateBorderSize=20, doApplyCalibration=True) 

745 psf = template.psf 

746 psfAvgPos = psf.getAveragePosition() 

747 psfSize = getPsfFwhm(template.psf) 

748 psfImg = psf.computeKernelImage(psfAvgPos) 

749 

750 # Generate a random aperture correction map 

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

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

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

754 science.info.setApCorrMap(apCorrMap) 

755 

756 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

757 config.mode = "convolveScience" 

758 

759 def _run_and_check_images(doDecorrelation): 

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

761 """ 

762 config.doDecorrelation = doDecorrelation 

763 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

765 if doDecorrelation: 

766 # Decorrelation requires recalculating the PSF, 

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

768 psfOutSize = getPsfFwhm(template.psf) 

769 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

770 else: 

771 psfOut = output.difference.psf 

772 psfAvgPos = psfOut.getAveragePosition() 

773 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

774 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

775 

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

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

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

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

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

781 

782 _run_and_check_images(doDecorrelation=True) 

783 _run_and_check_images(doDecorrelation=False) 

784 

785 def _compare_apCorrMaps(self, a, b): 

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

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

788 

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

790 

791 Parameters 

792 ---------- 

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

794 The two aperture correction maps to compare. 

795 """ 

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

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

798 value2 = b.get(name) 

799 self.assertIsNotNone(value2) 

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

801 self.assertFloatsAlmostEqual( 

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

803 

804 def test_fake_mask_plane_propagation(self): 

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

806 This is testing method called updateMasks 

807 """ 

808 xSize = 200 

809 ySize = 200 

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

811 science_fake_img, science_fake_sources = makeTestImage( 

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

813 ) 

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

815 tmplt_fake_img, tmplt_fake_sources = makeTestImage( 

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

817 ) 

818 # created fakes and added them to the images 

819 science.image.array += science_fake_img.image.array 

820 template.image.array += tmplt_fake_img.image.array 

821 

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

823 # adding mask planes to both science and template images 

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

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

826 

827 for a_science_source in science_fake_sources: 

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

829 bbox = lsst.geom.Box2I( 

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

831 ) 

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

833 

834 for a_template_source in tmplt_fake_sources: 

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

836 bbox = lsst.geom.Box2I( 

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

838 lsst.geom.Extent2I(3, 3) 

839 ) 

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

841 

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

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

844 

845 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

846 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

848 

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

850 diff_mask = subtraction.difference.mask 

851 

852 # science mask should be now in INJECTED 

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

854 

855 # template mask should be now in INJECTED_TEMPLATE 

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

857 

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

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

860 

861 

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

863 

864 def test_mismatched_template(self): 

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

866 does not fully contain the science image. 

867 """ 

868 xSize = 200 

869 ySize = 200 

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

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

872 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

873 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

874 with self.assertRaises(AssertionError): 

875 task.run(template, science, sources) 

876 

877 def test_equal_images(self): 

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

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

880 """ 

881 noiseLevel = 1. 

882 xSize = 400 

883 ySize = 400 

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

885 xSize=xSize, ySize=ySize) 

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

887 templateBorderSize=20, doApplyCalibration=True, 

888 xSize=xSize, ySize=ySize) 

889 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

890 config.doSubtractBackground = False 

891 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

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

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

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

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

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

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

899 scoreMean = computeRobustStatistics(output.scoreExposure.image, 

900 output.scoreExposure.mask, 

901 statsCtrl) 

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

903 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

906 statsCtrl=statsCtrl, statistic=afwMath.STDEV) 

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

908 

909 def test_incomplete_template_coverage(self): 

910 noiseLevel = 1. 

911 border = 20 

912 xSize = 400 

913 ySize = 400 

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

915 xSize=xSize, ySize=ySize) 

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

917 templateBorderSize=border, doApplyCalibration=True, 

918 xSize=xSize, ySize=ySize) 

919 

920 science_height = science.getBBox().getDimensions().getY() 

921 

922 def _run_and_check_coverage(template_coverage): 

923 template_cut = template.clone() 

924 template_height = int(science_height*template_coverage + border) 

925 template_cut.image.array[:, template_height:] = 0 

926 template_cut.mask.array[:, template_height:] = template_cut.mask.getPlaneBitMask('NO_DATA') 

927 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

928 if template_coverage < config.requiredTemplateFraction: 

929 doRaise = True 

930 elif template_coverage < config.minTemplateFractionForExpectedSuccess: 

931 doRaise = True 

932 else: 

933 doRaise = False 

934 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

935 if doRaise: 

936 with self.assertRaises(NoWorkFound): 

937 task.run(template_cut, science.clone(), sources.copy(deep=True)) 

938 else: 

939 task.run(template_cut, science.clone(), sources.copy(deep=True)) 

940 _run_and_check_coverage(template_coverage=0.09) 

941 _run_and_check_coverage(template_coverage=0.19) 

942 _run_and_check_coverage(template_coverage=.7) 

943 

944 def test_clear_template_mask(self): 

945 noiseLevel = 1. 

946 xSize = 400 

947 ySize = 400 

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

949 xSize=xSize, ySize=ySize) 

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

951 templateBorderSize=20, doApplyCalibration=True, 

952 xSize=xSize, ySize=ySize) 

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

954 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

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

956 mask = template.mask 

957 x0 = 50 

958 x1 = 75 

959 y0 = 150 

960 y1 = 175 

961 scienceMaskCheck = {} 

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

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

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

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

966 

967 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

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

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

971 if maskPlane in diffimEmptyMaskPlanes: 

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

973 elif maskPlane in config.preserveTemplateMask: 

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

975 else: 

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

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

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

979 diffimMask = output.scoreExposure.mask 

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

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

982 if maskPlane in diffimEmptyMaskPlanes: 

983 self.assertEqual(diffimSum, 0) 

984 else: 

985 self.assertTrue(diffimSum >= scienceSum) 

986 

987 def test_agnostic_template_psf(self): 

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

989 larger or smaller than the science image PSF. 

990 """ 

991 noiseLevel = .3 

992 xSize = 400 

993 ySize = 400 

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

995 noiseSeed=6, templateBorderSize=0, 

996 xSize=xSize, ySize=ySize) 

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

998 noiseSeed=7, doApplyCalibration=True, 

999 xSize=xSize, ySize=ySize) 

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

1001 noiseSeed=8, doApplyCalibration=True, 

1002 xSize=xSize, ySize=ySize) 

1003 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1004 config.doSubtractBackground = False 

1005 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

1006 

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

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

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

1010 

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

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

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

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

1015 

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

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

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

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

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

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

1022 statistic=afwMath.STDEV) 

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

1024 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

1027 

1028 def test_few_sources(self): 

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

1030 """ 

1031 xSize = 256 

1032 ySize = 256 

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

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

1035 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1036 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

1037 sources = sources[0:1] 

1038 with self.assertRaisesRegex(RuntimeError, 

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

1040 task.run(template, science, sources) 

1041 

1042 def test_background_subtraction(self): 

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

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

1045 """ 

1046 noiseLevel = 1. 

1047 xSize = 512 

1048 ySize = 512 

1049 x0 = 123 

1050 y0 = 456 

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

1052 templateBorderSize=20, 

1053 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

1054 doApplyCalibration=True) 

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

1056 

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

1058 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

1060 background=background_model, 

1061 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

1062 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1063 config.doSubtractBackground = True 

1064 

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

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

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

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

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

1070 

1071 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

1073 

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

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

1076 

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

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

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

1080 

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

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

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

1084 statsCtrl, statistic=afwMath.STDEV) 

1085 # get the img psf Noise Equivalent Area value 

1086 nea = computePSFNoiseEquivalentArea(science.psf) 

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

1088 

1089 def test_scale_variance(self): 

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

1091 """ 

1092 scienceNoiseLevel = 4. 

1093 templateNoiseLevel = 2. 

1094 scaleFactor = 1.345 

1095 xSize = 400 

1096 ySize = 400 

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

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

1099 

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

1101 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

1104 """ 

1105 

1106 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1107 config.doSubtractBackground = False 

1108 config.doDecorrelation = doDecorrelation 

1109 config.doScaleVariance = doScaleVariance 

1110 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

1112 if doScaleVariance: 

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

1114 scaleFactor, atol=0.05) 

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

1116 scaleFactor, atol=0.05) 

1117 

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

1119 # get the img psf Noise Equivalent Area value 

1120 nea = computePSFNoiseEquivalentArea(science.psf) 

1121 scienceNoise /= nea 

1122 if doDecorrelation: 

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

1124 templateNoise /= nea 

1125 else: 

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

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

1128 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

1129 output.matchedTemplate.mask, 

1130 statsCtrl) 

1131 if doScaleVariance: 

1132 templateNoise *= scaleFactor 

1133 scienceNoise *= scaleFactor 

1134 varMean = computeRobustStatistics(output.scoreExposure.variance, 

1135 output.scoreExposure.mask, 

1136 statsCtrl) 

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

1138 

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

1140 xSize=xSize, ySize=ySize) 

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

1142 templateBorderSize=20, doApplyCalibration=True, 

1143 xSize=xSize, ySize=ySize) 

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

1145 # when the template and science variance planes are correct 

1146 _run_and_check_images(science, template, sources, statsCtrl, 

1147 doDecorrelation=True, doScaleVariance=True) 

1148 _run_and_check_images(science, template, sources, statsCtrl, 

1149 doDecorrelation=True, doScaleVariance=False) 

1150 _run_and_check_images(science, template, sources, statsCtrl, 

1151 doDecorrelation=False, doScaleVariance=True) 

1152 _run_and_check_images(science, template, sources, statsCtrl, 

1153 doDecorrelation=False, doScaleVariance=False) 

1154 

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

1156 # when the template variance plane is incorrect 

1157 template.variance.array /= scaleFactor 

1158 science.variance.array /= scaleFactor 

1159 _run_and_check_images(science, template, sources, statsCtrl, 

1160 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

1161 _run_and_check_images(science, template, sources, statsCtrl, 

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

1163 _run_and_check_images(science, template, sources, statsCtrl, 

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

1165 _run_and_check_images(science, template, sources, statsCtrl, 

1166 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

1167 

1168 def test_exposure_properties(self): 

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

1170 with the Score image. 

1171 """ 

1172 noiseLevel = 1. 

1173 xSize = 400 

1174 ySize = 400 

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

1176 xSize=xSize, ySize=ySize) 

1177 psf = science.psf 

1178 psfAvgPos = psf.getAveragePosition() 

1179 psfSize = getPsfFwhm(science.psf) 

1180 psfImg = psf.computeKernelImage(psfAvgPos) 

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

1182 templateBorderSize=20, doApplyCalibration=True, 

1183 xSize=xSize, ySize=ySize) 

1184 

1185 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1186 

1187 def _run_and_check_images(doDecorrelation): 

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

1189 """ 

1190 config.doDecorrelation = doDecorrelation 

1191 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

1193 psfOut = output.scoreExposure.psf 

1194 psfAvgPos = psfOut.getAveragePosition() 

1195 if doDecorrelation: 

1196 # Decorrelation requires recalculating the PSF, 

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

1198 psfOutSize = getPsfFwhm(science.psf) 

1199 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

1200 else: 

1201 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

1202 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

1203 

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

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

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

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

1208 _run_and_check_images(doDecorrelation=True) 

1209 _run_and_check_images(doDecorrelation=False) 

1210 

1211 

1212def setup_module(module): 

1213 lsst.utils.tests.init() 

1214 

1215 

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

1217 pass 

1218 

1219 

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

1221 lsst.utils.tests.init() 

1222 unittest.main()