Coverage for tests/test_subtractTask.py: 99%

804 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-07 23:33 +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 

24from astropy import units as u 

25 

26import lsst.afw.detection as afwDetection 

27import lsst.afw.image as afwImage 

28import lsst.afw.math as afwMath 

29import lsst.afw.table as afwTable 

30import lsst.geom 

31import lsst.meas.algorithms as measAlg 

32from lsst.ip.diffim import subtractImages, InsufficientKernelSourcesError 

33from lsst.pex.config import FieldValidationError 

34from lsst.pipe.base import NoWorkFound 

35import lsst.utils.tests 

36import numpy as np 

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

38 evaluateMeanPsfFwhm, getPsfFwhm) 

39from lsst.pex.exceptions import InvalidParameterError 

40 

41from utils import makeStats, makeTestImage, CustomCoaddPsf 

42 

43 

44class AlardLuptonSubtractTestBase: 

45 goodPsfSize = 2.0 

46 midPsfSize = 2.4 

47 badPsfSize = 2.8 

48 

49 def _setup_subtraction(self, fluxField="truth_instFlux", errField="truth_instFluxErr", **kwargs): 

50 """Setup and configure the image subtraction PipelineTask. 

51 

52 Parameters 

53 ---------- 

54 fluxField : `str`, optional 

55 Name of the flux field in the source catalog. 

56 errField : `str`, optional 

57 Name of the flux error field in the source catalog. 

58 **kwargs 

59 Any additional config parameters to set. 

60 

61 Returns 

62 ------- 

63 `lsst.pipe.base.PipelineTask` 

64 The configured Task to use for detection and measurement. 

65 """ 

66 config = self.subtractTask.ConfigClass() 

67 config.doSubtractBackground = False 

68 config.restrictKernelEdgeSources = False 

69 config.sourceSelector.signalToNoise.fluxField = fluxField 

70 config.sourceSelector.signalToNoise.errField = errField 

71 config.sourceSelector.doUnresolved = True 

72 config.sourceSelector.doIsolated = True 

73 config.sourceSelector.doRequirePrimary = True 

74 config.sourceSelector.doFlags = True 

75 config.sourceSelector.doSignalToNoise = True 

76 config.sourceSelector.flags.bad = ["base_PsfFlux_flag", ] 

77 config.update(**kwargs) 

78 

79 return self.subtractTask(config=config) 

80 

81 

82class AlardLuptonSubtractTest(AlardLuptonSubtractTestBase, lsst.utils.tests.TestCase): 

83 subtractTask = subtractImages.AlardLuptonSubtractTask 

84 

85 def test_allowed_config_modes(self): 

86 """Verify the allowable modes for convolution. 

87 """ 

88 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

89 config.mode = 'auto' 

90 config.mode = 'convolveScience' 

91 config.mode = 'convolveTemplate' 

92 

93 with self.assertRaises(FieldValidationError): 

94 config.mode = 'aotu' 

95 

96 def test_mismatched_template(self): 

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

98 does not fully contain the science image. 

99 """ 

100 xSize = 200 

101 ySize = 200 

102 science, sources = makeTestImage(psfSize=self.midPsfSize, xSize=xSize + 20, ySize=ySize + 20) 

103 template, _ = makeTestImage(psfSize=self.midPsfSize, xSize=xSize, ySize=ySize, 

104 doApplyCalibration=True) 

105 task = self._setup_subtraction() 

106 with self.assertRaises(AssertionError): 

107 task.run(template, science, sources) 

108 

109 def test_mismatched_filter(self): 

110 """Test that an error is raised if the science and template have 

111 different bands. 

112 """ 

113 xSize = 200 

114 ySize = 200 

115 science, sources = makeTestImage(psfSize=self.midPsfSize, xSize=xSize + 20, ySize=ySize + 20, 

116 band="g", physicalFilter="g noCamera") 

117 template, _ = makeTestImage(psfSize=self.midPsfSize, xSize=xSize, ySize=ySize, 

118 doApplyCalibration=True, band="not-g", physicalFilter="not-g noCamera") 

119 task = self._setup_subtraction() 

120 with self.assertRaises(AssertionError): 

121 task.run(template, science, sources) 

122 

123 def test_incomplete_template_coverage(self): 

124 noiseLevel = 1. 

125 border = 20 

126 xSize = 400 

127 ySize = 400 

128 nSources = 80 

129 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

130 nSrc=nSources, xSize=xSize, ySize=ySize) 

131 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

132 nSrc=nSources, templateBorderSize=border, doApplyCalibration=True, 

133 xSize=xSize, ySize=ySize) 

134 

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

136 

137 def _run_and_check_coverage(template_coverage, 

138 requiredTemplateFraction=0.1, 

139 minTemplateFractionForExpectedSuccess=0.2): 

140 template_cut = template.clone() 

141 template_height = int(science_height*template_coverage + border) 

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

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

144 task = self._setup_subtraction( 

145 requiredTemplateFraction=requiredTemplateFraction, 

146 minTemplateFractionForExpectedSuccess=minTemplateFractionForExpectedSuccess 

147 ) 

148 if template_coverage < requiredTemplateFraction: 

149 doRaise = True 

150 elif template_coverage < minTemplateFractionForExpectedSuccess: 

151 doRaise = True 

152 else: 

153 doRaise = False 

154 if doRaise: 

155 with self.assertRaises(NoWorkFound): 

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

157 else: 

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

159 _run_and_check_coverage(template_coverage=0.09) 

160 _run_and_check_coverage(template_coverage=0.15) 

161 _run_and_check_coverage(template_coverage=0.7) 

162 

163 def test_clear_template_mask(self): 

164 noiseLevel = 1. 

165 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

166 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

167 templateBorderSize=20, doApplyCalibration=True) 

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

169 task = self._setup_subtraction(mode="convolveTemplate") 

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

171 mask = template.mask 

172 x0 = 50 

173 x1 = 75 

174 y0 = 150 

175 y1 = 175 

176 scienceMaskCheck = {} 

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

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

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

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

181 

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

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

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

185 if maskPlane in diffimEmptyMaskPlanes: 

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

187 elif maskPlane in task.config.preserveTemplateMask: 

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

189 else: 

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

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

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

193 diffimMask = output.difference.mask 

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

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

196 if maskPlane in diffimEmptyMaskPlanes: 

197 self.assertEqual(diffimSum, 0) 

198 else: 

199 self.assertTrue(diffimSum >= scienceSum) 

200 

201 def test_equal_images(self): 

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

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

204 """ 

205 noiseLevel = 1. 

206 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

207 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

208 templateBorderSize=20, doApplyCalibration=True) 

209 task = self._setup_subtraction() 

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

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

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

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

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

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

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

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

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

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

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

221 makeStats(), statistic=afwMath.STDEV) 

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

223 

224 def test_equal_images_missing_mask_planes(self): 

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

226 with the same size psf in the template and science and with missing 

227 mask planes. 

228 """ 

229 noiseLevel = 1. 

230 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

231 addMaskPlanes=[]) 

232 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

233 templateBorderSize=20, doApplyCalibration=True, addMaskPlanes=[]) 

234 task = self._setup_subtraction() 

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

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

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

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

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

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

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

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

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

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

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

246 makeStats(), statistic=afwMath.STDEV) 

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

248 

249 def test_psf_size(self): 

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

251 fwhmExposureBuffer and fwhmExposureGrid parameters are set. 

252 """ 

253 noiseLevel = 1. 

254 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

255 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

256 templateBorderSize=20, doApplyCalibration=True) 

257 

258 schema = afwTable.ExposureTable.makeMinimalSchema() 

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

260 exposureCatalog = afwTable.ExposureCatalog(schema) 

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

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

263 

264 record = exposureCatalog.addNew() 

265 record.setPsf(psf) 

266 record.setWcs(template.wcs) 

267 record.setD(weightKey, 1.0) 

268 record.setBBox(template.getBBox()) 

269 

270 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs) 

271 template.setPsf(customPsf) 

272 

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

274 with self.assertRaises(InvalidParameterError): 

275 getPsfFwhm(template.psf, True) 

276 

277 with self.assertRaises(InvalidParameterError): 

278 getPsfFwhm(template.psf, False) 

279 

280 # Test that evaluateMeanPsfFwhm runs successfully on the template. 

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

282 

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

284 # all points in the science image. 

285 fwhm1 = getPsfFwhm(science.psf, False) 

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

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

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

289 

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

291 fwhmExposureGrid=10), 

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

293 ) 

294 

295 # Test that the image subtraction task runs successfully. 

296 task = self._setup_subtraction() 

297 

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

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

300 task.run(template, science, sources) 

301 

302 # Check that evaluateMeanPsfFwhm was called. 

303 # This tests that getPsfFwhm failed raising InvalidParameterError, 

304 # that is caught and handled appropriately. 

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

306 "Evaluting PSF on a grid of points." 

307 ) 

308 self.assertIn(logMessage, cm.output) 

309 

310 def test_auto_convolveTemplate(self): 

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

312 the template psf is the smaller. 

313 """ 

314 noiseLevel = 1. 

315 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

316 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

317 templateBorderSize=20, doApplyCalibration=True) 

318 task = self._setup_subtraction(mode="convolveTemplate") 

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

320 

321 task = self._setup_subtraction(mode="auto") 

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

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

324 

325 def test_auto_convolveScience(self): 

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

327 the science psf is the smaller. 

328 """ 

329 noiseLevel = 1. 

330 science, sources = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

331 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

332 templateBorderSize=20, doApplyCalibration=True) 

333 task = self._setup_subtraction(mode="convolveScience") 

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

335 

336 task = self._setup_subtraction(mode="auto") 

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

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

339 

340 def test_science_better(self): 

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

342 with the science psf being smaller than the template. 

343 """ 

344 statsCtrl = makeStats() 

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

346 

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

348 science, sources = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=scienceNoiseLevel, 

349 noiseSeed=6) 

350 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=templateNoiseLevel, noiseSeed=7, 

351 templateBorderSize=20, doApplyCalibration=True) 

352 task = self._setup_subtraction(mode="convolveScience") 

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

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

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

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

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

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

359 statsCtrlDetect) 

360 

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

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

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

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

365 statsCtrl) 

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

367 statsCtrl, statistic=afwMath.STDEV) 

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

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

370 

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

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

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

374 

375 def test_template_better(self): 

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

377 with the template psf being smaller than the science. 

378 """ 

379 statsCtrl = makeStats() 

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

381 

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

383 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=scienceNoiseLevel, 

384 noiseSeed=6) 

385 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=templateNoiseLevel, noiseSeed=7, 

386 templateBorderSize=20, doApplyCalibration=True) 

387 task = self._setup_subtraction() 

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

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

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

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

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

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

394 

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

396 statsCtrlDetect) 

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

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

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

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

401 statsCtrl) 

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

403 statsCtrl, statistic=afwMath.STDEV) 

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

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

406 

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

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

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

410 

411 def test_symmetry(self): 

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

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

414 should be nearly the same. 

415 """ 

416 noiseLevel = 1. 

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

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

419 science, sources = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, 

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

421 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, 

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

423 task = self._setup_subtraction(mode='auto') 

424 

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

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

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

428 

429 delta = template_better.difference.clone() 

430 delta.image -= science_better.difference.image 

431 delta.variance -= science_better.difference.variance 

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

433 

434 statsCtrl = makeStats() 

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

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

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

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

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

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

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

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

443 

444 def test_few_sources(self): 

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

446 """ 

447 xSize = 256 

448 ySize = 256 

449 science, sources = makeTestImage(psfSize=self.midPsfSize, nSrc=10, xSize=xSize, ySize=ySize) 

450 template, _ = makeTestImage(psfSize=self.goodPsfSize, nSrc=10, xSize=xSize, ySize=ySize, 

451 doApplyCalibration=True) 

452 task = self._setup_subtraction() 

453 sources = sources[0:1] 

454 with self.assertRaises(InsufficientKernelSourcesError): 

455 task.run(template, science, sources) 

456 

457 def test_kernel_source_selector(self): 

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

459 """ 

460 xSize = 256 

461 ySize = 256 

462 nSourcesSimulated = 20 

463 sciencePsfSize = self.midPsfSize 

464 templatePsfSize = self.goodPsfSize 

465 science, sources = makeTestImage(psfSize=sciencePsfSize, nSrc=nSourcesSimulated, 

466 xSize=xSize, ySize=ySize) 

467 template, _ = makeTestImage(psfSize=templatePsfSize, nSrc=nSourcesSimulated, 

468 xSize=xSize, ySize=ySize, doApplyCalibration=True) 

469 

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

471 sources = sourcesIn.copy(deep=True) 

472 

473 task = self._setup_subtraction(maxKernelSources=maxKernelSources, 

474 minKernelSources=minKernelSources, 

475 ) 

476 task.templatePsfSize = templatePsfSize 

477 task.sciencePsfSize = sciencePsfSize 

478 task.matchedPsfSize = sciencePsfSize 

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

480 # Note that this will use the last flag in the list for the rest of 

481 # the test. 

482 for badSourceFlag in task.sourceSelector.config.flags.bad: 

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

484 nSources = len(sources) 

485 # Flag a third of the sources 

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

487 rejectRadius = 2*task.config.makeKernel.kernel.active.kernelSize 

488 bbox = science.getBBox() 

489 bbox.grow(-rejectRadius) 

490 edgeSources = ~bbox.contains(sources.getX(), sources.getY()) 

491 sources[edgeSources][badSourceFlag] = True 

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

493 if maxKernelSources > 0: 

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

495 else: 

496 nGoodSources = nSources - nBadSources 

497 

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

499 signalToNoise = signalToNoise[~sources[badSourceFlag]] 

500 signalToNoise.sort() 

501 selectSources = task._sourceSelector(template, science, sources) 

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

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

504 signalToNoiseOut.sort() 

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

506 

507 _run_and_check_sources(sources) 

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

509 _run_and_check_sources(sources, maxKernelSources=-1) 

510 with self.assertRaises(RuntimeError): 

511 _run_and_check_sources(sources, minKernelSources=1000) 

512 

513 def test_order_equal_images(self): 

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

515 if the images are equivalent. 

516 """ 

517 noiseLevel = .1 

518 seed1 = 6 

519 seed2 = 7 

520 for psfSize in [self.midPsfSize, self.goodPsfSize, self.badPsfSize]: 

521 science1, sources1 = makeTestImage(psfSize=psfSize, noiseLevel=noiseLevel, noiseSeed=seed1, 

522 clearEdgeMask=True) 

523 template1, _ = makeTestImage(psfSize=psfSize, noiseLevel=noiseLevel, noiseSeed=seed2, 

524 templateBorderSize=0, doApplyCalibration=True, 

525 clearEdgeMask=True) 

526 task1 = self._setup_subtraction(mode="convolveTemplate") 

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

528 

529 science2, sources2 = makeTestImage(psfSize=psfSize, noiseLevel=noiseLevel, noiseSeed=seed1, 

530 clearEdgeMask=True) 

531 template2, _ = makeTestImage(psfSize=psfSize, noiseLevel=noiseLevel, noiseSeed=seed2, 

532 templateBorderSize=0, doApplyCalibration=True, 

533 clearEdgeMask=True) 

534 task2 = self._setup_subtraction(mode="convolveScience") 

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

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

537 results_convolveScience.difference.getBBox()) 

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

539 diff1 -= template1.maskedImage[bbox] 

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

541 diff2 -= template2.maskedImage[bbox] 

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

543 diff1.image.array, 

544 atol=noiseLevel*5.) 

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

546 diff2.image.array, 

547 atol=noiseLevel*5.) 

548 diffErr = noiseLevel*2 

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

550 results_convolveScience.difference[bbox].maskedImage, 

551 atol=diffErr*5.) 

552 

553 def test_background_subtraction(self): 

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

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

556 

557 NOTE: Background subtraction is now turned off by default in 

558 subtractImages. It is now run in detectAndMeasure instead, but since the 

559 code to run background subtraction is not being removed this test should 

560 stay to make sure it continues functioning as intended. 

561 """ 

562 noiseLevel = 1. 

563 xSize = 512 

564 ySize = 512 

565 x0 = 123 

566 y0 = 456 

567 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

568 templateBorderSize=20, 

569 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

570 doApplyCalibration=True) 

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

572 

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

574 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

575 science, sources = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

576 background=background_model, 

577 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

578 # Don't use ``self._setup_subtraction()`` here. 

579 # Modifying the config of a subtask is messy. 

580 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

581 

582 config.sourceSelector.signalToNoise.fluxField = "truth_instFlux" 

583 config.sourceSelector.signalToNoise.errField = "truth_instFluxErr" 

584 config.doSubtractBackground = True 

585 

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

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

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

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

590 statsCtrl = makeStats() 

591 

592 def _run_and_check_images(config, statsCtrl, mode): 

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

594 """ 

595 config.mode = mode 

596 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

598 

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

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

601 

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

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

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

605 

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

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

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

609 statsCtrl, statistic=afwMath.STDEV) 

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

611 

612 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

613 _run_and_check_images(config, statsCtrl, "convolveScience") 

614 

615 def test_scale_variance_convolve_template(self): 

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

617 """ 

618 scienceNoiseLevel = 4. 

619 templateNoiseLevel = 2. 

620 scaleFactor = 1.345 

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

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

623 

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

625 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

628 """ 

629 

630 task = self._setup_subtraction(doDecorrelation=doDecorrelation, 

631 doScaleVariance=doScaleVariance, 

632 ) 

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

634 if doScaleVariance: 

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

636 scaleFactor, atol=0.05) 

637 

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

639 if doDecorrelation: 

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

641 else: 

642 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

643 output.matchedTemplate.mask, 

644 statsCtrl) 

645 

646 if doScaleVariance: 

647 # Only the science variance is scaled here. The template 

648 # variance is scaled independently in ``GetTemplateTask``. 

649 scienceNoise *= scaleFactor 

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

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

652 

653 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=scienceNoiseLevel, noiseSeed=6) 

654 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=templateNoiseLevel, noiseSeed=7, 

655 templateBorderSize=20, doApplyCalibration=True) 

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

657 # when the template and science variance planes are correct 

658 _run_and_check_images(science, template, sources, statsCtrl, 

659 doDecorrelation=True, doScaleVariance=True) 

660 _run_and_check_images(science, template, sources, statsCtrl, 

661 doDecorrelation=True, doScaleVariance=False) 

662 _run_and_check_images(science, template, sources, statsCtrl, 

663 doDecorrelation=False, doScaleVariance=True) 

664 _run_and_check_images(science, template, sources, statsCtrl, 

665 doDecorrelation=False, doScaleVariance=False) 

666 

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

668 # when the input science variance plane is incorrect 

669 science.variance.array /= scaleFactor 

670 _run_and_check_images(science, template, sources, statsCtrl, 

671 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

672 _run_and_check_images(science, template, sources, statsCtrl, 

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

674 _run_and_check_images(science, template, sources, statsCtrl, 

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

676 _run_and_check_images(science, template, sources, statsCtrl, 

677 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

678 

679 def test_scale_variance_convolve_science(self): 

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

681 """ 

682 scienceNoiseLevel = 4. 

683 templateNoiseLevel = 2. 

684 scaleFactor = 1.345 

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

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

687 

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

689 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

692 """ 

693 

694 task = self._setup_subtraction(mode="convolveScience", 

695 doDecorrelation=doDecorrelation, 

696 doScaleVariance=doScaleVariance, 

697 restrictKernelEdgeSources=False, 

698 ) 

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

700 if doScaleVariance: 

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

702 scaleFactor, atol=0.05) 

703 

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

705 if doDecorrelation: 

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

707 else: 

708 scienceNoise = computeRobustStatistics(output.matchedScience.variance, 

709 output.matchedScience.mask, 

710 statsCtrl) 

711 

712 if doScaleVariance: 

713 # Only the science variance is scaled here. The template 

714 # variance is scaled independently in ``GetTemplateTask``. 

715 scienceNoise *= scaleFactor 

716 

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

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

719 

720 science, sources = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=scienceNoiseLevel, noiseSeed=6) 

721 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=templateNoiseLevel, noiseSeed=7, 

722 templateBorderSize=20, doApplyCalibration=True) 

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

724 # when the template and science variance planes are correct 

725 _run_and_check_images(science, template, sources, statsCtrl, 

726 doDecorrelation=True, doScaleVariance=True) 

727 _run_and_check_images(science, template, sources, statsCtrl, 

728 doDecorrelation=True, doScaleVariance=False) 

729 _run_and_check_images(science, template, sources, statsCtrl, 

730 doDecorrelation=False, doScaleVariance=True) 

731 _run_and_check_images(science, template, sources, statsCtrl, 

732 doDecorrelation=False, doScaleVariance=False) 

733 

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

735 # when the input science variance plane is incorrect 

736 science.variance.array /= scaleFactor 

737 _run_and_check_images(science, template, sources, statsCtrl, 

738 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

739 _run_and_check_images(science, template, sources, statsCtrl, 

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

741 _run_and_check_images(science, template, sources, statsCtrl, 

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

743 _run_and_check_images(science, template, sources, statsCtrl, 

744 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

745 

746 def test_exposure_properties_convolve_template(self): 

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

748 when the template is convolved. 

749 """ 

750 noiseLevel = 1. 

751 seed = 37 

752 rng = np.random.RandomState(seed) 

753 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

754 psf = science.psf 

755 psfAvgPos = psf.getAveragePosition() 

756 psfSize = getPsfFwhm(science.psf) 

757 psfImg = psf.computeKernelImage(psfAvgPos) 

758 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

759 templateBorderSize=20, doApplyCalibration=True) 

760 

761 # Generate a random aperture correction map 

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

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

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

765 science.info.setApCorrMap(apCorrMap) 

766 

767 def _run_and_check_images(doDecorrelation): 

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

769 """ 

770 task = self._setup_subtraction(mode="convolveTemplate", 

771 doDecorrelation=doDecorrelation, 

772 ) 

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

774 psfOut = output.difference.psf 

775 psfAvgPos = psfOut.getAveragePosition() 

776 if doDecorrelation: 

777 # Decorrelation requires recalculating the PSF, 

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

779 psfOutSize = getPsfFwhm(science.psf) 

780 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

781 else: 

782 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

783 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

784 

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

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

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

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

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

790 _run_and_check_images(doDecorrelation=True) 

791 _run_and_check_images(doDecorrelation=False) 

792 

793 def test_exposure_properties_convolve_science(self): 

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

795 when the science image is convolved. 

796 """ 

797 noiseLevel = 1. 

798 seed = 37 

799 rng = np.random.RandomState(seed) 

800 science, sources = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

801 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

802 templateBorderSize=20, doApplyCalibration=True) 

803 psf = template.psf 

804 psfAvgPos = psf.getAveragePosition() 

805 psfSize = getPsfFwhm(template.psf) 

806 psfImg = psf.computeKernelImage(psfAvgPos) 

807 

808 # Generate a random aperture correction map 

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

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

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

812 science.info.setApCorrMap(apCorrMap) 

813 

814 def _run_and_check_images(doDecorrelation): 

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

816 """ 

817 task = self._setup_subtraction(mode="convolveScience", 

818 doDecorrelation=doDecorrelation, 

819 ) 

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

821 if doDecorrelation: 

822 # Decorrelation requires recalculating the PSF, 

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

824 psfOutSize = getPsfFwhm(template.psf) 

825 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

826 else: 

827 psfOut = output.difference.psf 

828 psfAvgPos = psfOut.getAveragePosition() 

829 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

830 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

831 

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

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

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

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

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

837 

838 _run_and_check_images(doDecorrelation=True) 

839 _run_and_check_images(doDecorrelation=False) 

840 

841 def _compare_apCorrMaps(self, a, b): 

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

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

844 

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

846 

847 Parameters 

848 ---------- 

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

850 The two aperture correction maps to compare. 

851 """ 

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

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

854 value2 = b.get(name) 

855 self.assertIsNotNone(value2) 

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

857 self.assertFloatsAlmostEqual( 

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

859 

860 def test_fake_mask_plane_propagation(self): 

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

862 This is testing method called updateMasks 

863 """ 

864 xSize = 200 

865 ySize = 200 

866 science, sources = makeTestImage(psfSize=self.midPsfSize, xSize=xSize, ySize=ySize) 

867 science_fake_img, science_fake_sources = makeTestImage( 

868 psfSize=self.midPsfSize, xSize=xSize, ySize=ySize, seed=7, nSrc=2, noiseLevel=0.25, fluxRange=1 

869 ) 

870 template, _ = makeTestImage(psfSize=self.midPsfSize, xSize=xSize, ySize=ySize, 

871 doApplyCalibration=True) 

872 tmplt_fake_img, tmplt_fake_sources = makeTestImage( 

873 psfSize=self.midPsfSize, xSize=xSize, ySize=ySize, seed=9, nSrc=2, noiseLevel=0.25, fluxRange=1 

874 ) 

875 # created fakes and added them to the images 

876 science.image.array += science_fake_img.image.array 

877 template.image.array += tmplt_fake_img.image.array 

878 

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

880 # adding mask planes to both science and template images 

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

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

883 

884 for a_science_source in science_fake_sources: 

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

886 bbox = lsst.geom.Box2I( 

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

888 ) 

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

890 

891 for a_template_source in tmplt_fake_sources: 

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

893 bbox = lsst.geom.Box2I( 

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

895 lsst.geom.Extent2I(3, 3) 

896 ) 

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

898 

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

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

901 

902 task = self._setup_subtraction() 

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

904 

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

906 diff_mask = subtraction.difference.mask 

907 

908 # science mask should be now in INJECTED 

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

910 

911 # template mask should be now in INJECTED_TEMPLATE 

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

913 

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

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

916 

917 def test_calculateMagLim(self): 

918 """Test that the limiting magnitude matches the analytic value for a 

919 Gaussian PSF and flat noise, and does not depend on the pixel units 

920 or on masked pixels. 

921 """ 

922 sigma = 1.5 

923 psfSigma = 2.0 

924 calibration = 3.0 

925 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(200, 200)) 

926 exposure = afwImage.ExposureF(bbox) 

927 exposure.variance.array[:, :] = sigma**2 

928 exposure.setPsf(afwDetection.GaussianPsf(41, 41, psfSigma)) 

929 exposure.setPhotoCalib(afwImage.PhotoCalib(calibration)) 

930 task = self._setup_subtraction() 

931 

932 maglim = task._calculateMagLim(exposure) 

933 fluxLim = 5*sigma*np.sqrt(4*np.pi*psfSigma**2) 

934 self.assertFloatsAlmostEqual(maglim, afwImage.PhotoCalib(calibration).instFluxToMagnitude(fluxLim), 

935 atol=1e-3) 

936 

937 # Pixels in other units, with a matching calibration. 

938 scaled = exposure.clone() 

939 scaled.maskedImage *= 10.0 

940 scaled.setPhotoCalib(afwImage.PhotoCalib(calibration/10.0)) 

941 self.assertFloatsAlmostEqual(task._calculateMagLim(scaled), maglim, atol=1e-6) 

942 

943 # Masked pixels do not count toward the noise. 

944 masked = exposure.clone() 

945 masked.variance.array[:, :100] = 1e6 

946 masked.mask.array[:, :100] |= masked.mask.getPlaneBitMask("NO_DATA") 

947 self.assertFloatsAlmostEqual(task._calculateMagLim(masked), maglim, atol=1e-6) 

948 

949 masked.mask.array[:, :] |= masked.mask.getPlaneBitMask("NO_DATA") 

950 self.assertTrue(np.isnan(task._calculateMagLim(masked))) 

951 

952 def test_metadata_metrics(self): 

953 """Verify fields are added to metadata when subtraction is run, and 

954 that the difference image limiting magnitude is calculated correctly, 

955 both with a "good" and "bad" seeing template. 

956 """ 

957 science, sources = makeTestImage(psfSize=self.badPsfSize, noiseLevel=1) 

958 template_good, _ = makeTestImage(psfSize=self.midPsfSize, doApplyCalibration=True, noiseLevel=0.25, 

959 templateBorderSize=20) 

960 template_bad, _ = makeTestImage(psfSize=9.5, doApplyCalibration=True, noiseLevel=0.25, 

961 templateBorderSize=20) 

962 

963 # Add a few sky objects; sky footprints are needed for some metrics. 

964 config = measAlg.SkyObjectsTask.ConfigClass() 

965 config.nSources = 3 

966 skyTask = measAlg.SkyObjectsTask(config=config, name="skySources") 

967 skyTask.skySourceKey = sources.schema["sky_source"].asKey() 

968 skyTask.run(science.mask, 10, catalog=sources) 

969 sources = sources.copy(deep=True) 

970 # Add centroids, since these sources were added post-measurement. 

971 for record in sources[sources["sky_source"]]: 

972 record["truth_x"] = record.getFootprint().getPeaks()[0].getFx() 

973 record["truth_y"] = record.getFootprint().getPeaks()[0].getFy() 

974 

975 # The metadata fields are attached to the subtractTask, so we do 

976 # need to run that; run it for both "good" and "bad" seeing templates 

977 

978 subtractTask_good = self._setup_subtraction() 

979 _ = subtractTask_good.run(template_good.clone(), science.clone(), sources) 

980 subtractTask_bad = self._setup_subtraction() 

981 _ = subtractTask_bad.run(template_bad.clone(), science.clone(), sources) 

982 

983 # Test that the diffim limiting magnitude combines the science and 

984 # template limits in flux. The limits depend on the variance and mask 

985 # planes, which `run` modifies, so take them from the metadata. 

986 def combineMagLims(metadata): 

987 fluxlim_science = (metadata['scienceLimitingMagnitude']*u.ABmag).to_value(u.nJy) 

988 fluxlim_template = (metadata['templateLimitingMagnitude']*u.ABmag).to_value(u.nJy) 

989 return (np.hypot(fluxlim_science, fluxlim_template)*u.nJy).to(u.ABmag).value 

990 

991 for task in (subtractTask_good, subtractTask_bad): 

992 self.assertFloatsAlmostEqual(task.metadata['diffimLimitingMagnitude'], 

993 combineMagLims(task.metadata), atol=1e-6) 

994 # The template with the wider PSF is shallower. 

995 self.assertLess(subtractTask_bad.metadata['templateLimitingMagnitude'], 

996 subtractTask_good.metadata['templateLimitingMagnitude']) 

997 

998 # Create a template with a PSF that is not defined at the image center. 

999 # First, make an exposure catalog so we can force the template to have 

1000 # a bad (off-image) PSF. It must have a record with a weight field 

1001 # and a BBox in order to let us set the PSF manually. 

1002 template_offimage, _ = makeTestImage() 

1003 schema = afwTable.ExposureTable.makeMinimalSchema() 

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

1005 exposureCatalog = afwTable.ExposureCatalog(schema) 

1006 record = exposureCatalog.addNew() 

1007 record.setD(weightKey, 1.0) 

1008 record.setBBox(template_offimage.getBBox()) 

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

1010 psf = measAlg.KernelPsf(kernel, template_offimage.getBBox().getCenter()) 

1011 record.setPsf(psf) 

1012 record.setWcs(template_offimage.wcs) 

1013 custom_offimage_psf = CustomCoaddPsf(exposureCatalog, template_offimage.wcs) 

1014 template_offimage.setPsf(custom_offimage_psf) 

1015 

1016 # Test that template PSF size edge cases are handled correctly. 

1017 subtractTask_offimage = self._setup_subtraction() 

1018 _ = subtractTask_offimage.run(template_offimage.clone(), science.clone(), sources) 

1019 # Test that providing no fallbackPsfSize results in a nan template 

1020 # limiting magnitude. 

1021 maglim_template_offimage = subtractTask_offimage._calculateMagLim(template_offimage) 

1022 self.assertTrue(np.isnan(maglim_template_offimage)) 

1023 # Test that the task falls back to the provided fallbackPsfSize, so 

1024 # the template and diffim limiting magnitudes are finite. 

1025 self.assertTrue(np.isfinite(subtractTask_offimage.metadata['templateLimitingMagnitude'])) 

1026 self.assertFloatsAlmostEqual(subtractTask_offimage.metadata['diffimLimitingMagnitude'], 

1027 combineMagLims(subtractTask_offimage.metadata), atol=1e-6) 

1028 

1029 # Test that several other expected metadata metrics exist 

1030 self.assertIn('scienceLimitingMagnitude', subtractTask_good.metadata) 

1031 self.assertIn('templateLimitingMagnitude', subtractTask_good.metadata) 

1032 

1033 # The mean ratio metric should be much worse on the "bad" subtraction. 

1034 self.assertLess(subtractTask_good.metadata['differenceFootprintRatioMean'], 0.02) 

1035 self.assertGreater(subtractTask_bad.metadata['differenceFootprintRatioMean'], 0.12) 

1036 

1037 

1038class AlardLuptonPreconvolveSubtractTest(AlardLuptonSubtractTestBase, lsst.utils.tests.TestCase): 

1039 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask 

1040 

1041 def test_mismatched_template(self): 

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

1043 does not fully contain the science image. 

1044 """ 

1045 xSize = 200 

1046 ySize = 200 

1047 science, sources = makeTestImage(psfSize=self.midPsfSize, xSize=xSize + 20, ySize=ySize + 20) 

1048 template, _ = makeTestImage(psfSize=self.midPsfSize, xSize=xSize, ySize=ySize, 

1049 doApplyCalibration=True) 

1050 task = self._setup_subtraction() 

1051 with self.assertRaises(AssertionError): 

1052 task.run(template, science, sources) 

1053 

1054 def test_equal_images(self): 

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

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

1057 """ 

1058 noiseLevel = 1. 

1059 xSize = 400 

1060 ySize = 400 

1061 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

1062 xSize=xSize, ySize=ySize) 

1063 template, _ = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

1064 templateBorderSize=20, doApplyCalibration=True, 

1065 xSize=xSize, ySize=ySize) 

1066 task = self._setup_subtraction() 

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

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

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

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

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

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

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

1074 scoreMean = computeRobustStatistics(output.scoreExposure.image, 

1075 output.scoreExposure.mask, 

1076 statsCtrl) 

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

1078 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

1081 statsCtrl=statsCtrl, statistic=afwMath.STDEV) 

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

1083 

1084 def test_incomplete_template_coverage(self): 

1085 noiseLevel = 1. 

1086 border = 20 

1087 xSize = 400 

1088 ySize = 400 

1089 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

1090 xSize=xSize, ySize=ySize) 

1091 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

1092 templateBorderSize=border, doApplyCalibration=True, 

1093 xSize=xSize, ySize=ySize) 

1094 

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

1096 

1097 def _run_and_check_coverage(template_coverage, 

1098 requiredTemplateFraction=0.1, 

1099 minTemplateFractionForExpectedSuccess=0.2): 

1100 template_cut = template.clone() 

1101 template_height = int(science_height*template_coverage + border) 

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

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

1104 task = self._setup_subtraction( 

1105 requiredTemplateFraction=requiredTemplateFraction, 

1106 minTemplateFractionForExpectedSuccess=minTemplateFractionForExpectedSuccess 

1107 ) 

1108 if template_coverage < task.config.requiredTemplateFraction: 

1109 doRaise = True 

1110 elif template_coverage < task.config.minTemplateFractionForExpectedSuccess: 

1111 doRaise = True 

1112 else: 

1113 doRaise = False 

1114 if doRaise: 

1115 with self.assertRaises(NoWorkFound): 

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

1117 else: 

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

1119 _run_and_check_coverage(template_coverage=0.09) 

1120 _run_and_check_coverage(template_coverage=0.15) 

1121 _run_and_check_coverage(template_coverage=.7) 

1122 

1123 def test_clear_template_mask(self): 

1124 noiseLevel = 1. 

1125 xSize = 400 

1126 ySize = 400 

1127 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

1128 xSize=xSize, ySize=ySize) 

1129 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

1130 templateBorderSize=20, doApplyCalibration=True, 

1131 xSize=xSize, ySize=ySize) 

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

1133 task = self._setup_subtraction() 

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

1135 mask = template.mask 

1136 x0 = 50 

1137 x1 = 75 

1138 y0 = 150 

1139 y1 = 175 

1140 scienceMaskCheck = {} 

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

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

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

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

1145 

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

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

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

1149 if maskPlane in diffimEmptyMaskPlanes: 

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

1151 elif maskPlane in task.config.preserveTemplateMask: 

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

1153 else: 

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

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

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

1157 diffimMask = output.scoreExposure.mask 

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

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

1160 if maskPlane in diffimEmptyMaskPlanes: 

1161 self.assertEqual(diffimSum, 0) 

1162 else: 

1163 self.assertTrue(diffimSum >= scienceSum) 

1164 

1165 def test_agnostic_template_psf(self): 

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

1167 larger or smaller than the science image PSF. 

1168 """ 

1169 noiseLevel = .3 

1170 xSize = 400 

1171 ySize = 400 

1172 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, 

1173 noiseSeed=6, templateBorderSize=0, 

1174 xSize=xSize, ySize=ySize) 

1175 template1, _ = makeTestImage(psfSize=self.badPsfSize, noiseLevel=noiseLevel, 

1176 noiseSeed=7, doApplyCalibration=True, 

1177 xSize=xSize, ySize=ySize) 

1178 template2, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, 

1179 noiseSeed=8, doApplyCalibration=True, 

1180 xSize=xSize, ySize=ySize) 

1181 task = self._setup_subtraction() 

1182 

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

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

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

1186 

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

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

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

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

1191 

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

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

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

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

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

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

1198 statistic=afwMath.STDEV) 

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

1200 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

1203 

1204 def test_few_sources(self): 

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

1206 """ 

1207 xSize = 256 

1208 ySize = 256 

1209 science, sources = makeTestImage(psfSize=self.midPsfSize, nSrc=10, xSize=xSize, ySize=ySize) 

1210 template, _ = makeTestImage(psfSize=self.goodPsfSize, nSrc=10, xSize=xSize, ySize=ySize, 

1211 doApplyCalibration=True) 

1212 task = self._setup_subtraction() 

1213 sources = sources[0:1] 

1214 with self.assertRaises(InsufficientKernelSourcesError): 

1215 task.run(template, science, sources) 

1216 

1217 def test_background_subtraction(self): 

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

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

1220 """ 

1221 noiseLevel = 1. 

1222 xSize = 512 

1223 ySize = 512 

1224 x0 = 123 

1225 y0 = 456 

1226 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

1227 templateBorderSize=20, 

1228 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

1229 doApplyCalibration=True) 

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

1231 

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

1233 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

1234 science, sources = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

1235 background=background_model, 

1236 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

1237 # Don't use ``self._setup_subtraction()`` here. 

1238 # Modifying the config of a subtask is messy. 

1239 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1240 

1241 config.sourceSelector.signalToNoise.fluxField = "truth_instFlux" 

1242 config.sourceSelector.signalToNoise.errField = "truth_instFluxErr" 

1243 config.doSubtractBackground = True 

1244 

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

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

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

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

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

1250 

1251 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

1253 

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

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

1256 

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

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

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

1260 

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

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

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

1264 statsCtrl, statistic=afwMath.STDEV) 

1265 # get the img psf Noise Equivalent Area value 

1266 nea = computePSFNoiseEquivalentArea(science.psf) 

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

1268 

1269 def test_scale_variance(self): 

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

1271 """ 

1272 scienceNoiseLevel = 4. 

1273 templateNoiseLevel = 2. 

1274 scaleFactor = 1.345 

1275 xSize = 400 

1276 ySize = 400 

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

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

1279 

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

1281 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

1284 """ 

1285 

1286 task = self._setup_subtraction(doDecorrelation=doDecorrelation, 

1287 doScaleVariance=doScaleVariance, 

1288 ) 

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

1290 if doScaleVariance: 

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

1292 scaleFactor, atol=0.05) 

1293 

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

1295 # get the img psf Noise Equivalent Area value 

1296 nea = computePSFNoiseEquivalentArea(science.psf) 

1297 scienceNoise /= nea 

1298 if doDecorrelation: 

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

1300 templateNoise /= nea 

1301 else: 

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

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

1304 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

1305 output.matchedTemplate.mask, 

1306 statsCtrl) 

1307 if doScaleVariance: 

1308 # Only the science variance is scaled here. The template 

1309 # variance is scaled independently in ``GetTemplateTask``. 

1310 scienceNoise *= scaleFactor 

1311 varMean = computeRobustStatistics(output.scoreExposure.variance, 

1312 output.scoreExposure.mask, 

1313 statsCtrl) 

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

1315 

1316 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=scienceNoiseLevel, noiseSeed=6, 

1317 xSize=xSize, ySize=ySize) 

1318 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=templateNoiseLevel, noiseSeed=7, 

1319 templateBorderSize=20, doApplyCalibration=True, 

1320 xSize=xSize, ySize=ySize) 

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

1322 # when the template and science variance planes are correct 

1323 _run_and_check_images(science, template, sources, statsCtrl, 

1324 doDecorrelation=True, doScaleVariance=True) 

1325 _run_and_check_images(science, template, sources, statsCtrl, 

1326 doDecorrelation=True, doScaleVariance=False) 

1327 _run_and_check_images(science, template, sources, statsCtrl, 

1328 doDecorrelation=False, doScaleVariance=True) 

1329 _run_and_check_images(science, template, sources, statsCtrl, 

1330 doDecorrelation=False, doScaleVariance=False) 

1331 

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

1333 # when the input science variance plane is incorrect 

1334 science.variance.array /= scaleFactor 

1335 _run_and_check_images(science, template, sources, statsCtrl, 

1336 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

1337 _run_and_check_images(science, template, sources, statsCtrl, 

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

1339 _run_and_check_images(science, template, sources, statsCtrl, 

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

1341 _run_and_check_images(science, template, sources, statsCtrl, 

1342 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

1343 

1344 def test_exposure_properties(self): 

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

1346 with the Score image. 

1347 """ 

1348 noiseLevel = 1. 

1349 xSize = 400 

1350 ySize = 400 

1351 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6, 

1352 xSize=xSize, ySize=ySize) 

1353 psf = science.psf 

1354 psfAvgPos = psf.getAveragePosition() 

1355 psfSize = getPsfFwhm(science.psf) 

1356 psfImg = psf.computeKernelImage(psfAvgPos) 

1357 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

1358 templateBorderSize=20, doApplyCalibration=True, 

1359 xSize=xSize, ySize=ySize) 

1360 

1361 def _run_and_check_images(doDecorrelation): 

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

1363 """ 

1364 task = self._setup_subtraction(doDecorrelation=doDecorrelation) 

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

1366 psfOut = output.scoreExposure.psf 

1367 psfAvgPos = psfOut.getAveragePosition() 

1368 if doDecorrelation: 

1369 # Decorrelation requires recalculating the PSF, 

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

1371 psfOutSize = getPsfFwhm(science.psf) 

1372 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

1373 else: 

1374 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

1375 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

1376 

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

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

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

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

1381 _run_and_check_images(doDecorrelation=True) 

1382 _run_and_check_images(doDecorrelation=False) 

1383 

1384 

1385class SimplifiedSubtractTest(AlardLuptonSubtractTestBase, lsst.utils.tests.TestCase): 

1386 subtractTask = subtractImages.SimplifiedSubtractTask 

1387 

1388 def test_runSimplifiedTaskWithExistingKernel(self): 

1389 """Test that the simplified task produces the same output as 

1390 `AlardLuptonSubtractTask` if it uses the AL kernel. 

1391 """ 

1392 noiseLevel = 1. 

1393 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

1394 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

1395 templateBorderSize=20, doApplyCalibration=True) 

1396 alTask = AlardLuptonSubtractTest._setup_subtraction(AlardLuptonSubtractTest()) 

1397 task = self._setup_subtraction(useExistingKernel=True) 

1398 

1399 alResults = alTask.run(template.clone(), science.clone(), sources) 

1400 results = task.run(template.clone(), science.clone(), 

1401 inputPsfMatchingKernel=alResults.psfMatchingKernel) 

1402 

1403 self.assertMaskedImagesEqual(alResults.difference, results.difference) 

1404 

1405 def test_runSimplifiedTaskWithSourceDetection(self): 

1406 """Test that the simplified task with source detection produces 

1407 reasonable output. 

1408 """ 

1409 noiseLevel = 1. 

1410 science, sources = makeTestImage(psfSize=self.midPsfSize, noiseLevel=noiseLevel, noiseSeed=6) 

1411 template, _ = makeTestImage(psfSize=self.goodPsfSize, noiseLevel=noiseLevel, noiseSeed=7, 

1412 templateBorderSize=20, doApplyCalibration=True) 

1413 task = self._setup_subtraction(useExistingKernel=False, 

1414 fluxField="base_PsfFlux_instFlux", 

1415 errField="base_PsfFlux_instFluxErr", 

1416 ) 

1417 

1418 output = task.run(template, science) 

1419 

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

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

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

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

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

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

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

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

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

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

1430 makeStats(), statistic=afwMath.STDEV) 

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

1432 

1433 

1434def setup_module(module): 

1435 lsst.utils.tests.init() 

1436 

1437 

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

1439 pass 

1440 

1441 

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

1443 lsst.utils.tests.init() 

1444 unittest.main()