Coverage for tests / test_subtractTask.py: 7%

790 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-26 09:17 +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.math as afwMath 

27import lsst.afw.table as afwTable 

28import lsst.geom 

29import lsst.meas.algorithms as measAlg 

30from lsst.ip.diffim import subtractImages, InsufficientKernelSourcesError 

31from lsst.pex.config import FieldValidationError 

32from lsst.pipe.base import NoWorkFound 

33import lsst.utils.tests 

34import numpy as np 

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

36 evaluateMeanPsfFwhm, getPsfFwhm) 

37from lsst.pex.exceptions import InvalidParameterError 

38 

39from utils import makeStats, makeTestImage, CustomCoaddPsf 

40 

41 

42class AlardLuptonSubtractTestBase: 

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

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

45 

46 Parameters 

47 ---------- 

48 fluxField : `str`, optional 

49 Name of the flux field in the source catalog. 

50 errField : `str`, optional 

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

52 **kwargs 

53 Any additional config parameters to set. 

54 

55 Returns 

56 ------- 

57 `lsst.pipe.base.PipelineTask` 

58 The configured Task to use for detection and measurement. 

59 """ 

60 config = self.subtractTask.ConfigClass() 

61 config.doSubtractBackground = False 

62 config.restrictKernelEdgeSources = False 

63 config.sourceSelector.signalToNoise.fluxField = fluxField 

64 config.sourceSelector.signalToNoise.errField = errField 

65 config.sourceSelector.doUnresolved = True 

66 config.sourceSelector.doIsolated = True 

67 config.sourceSelector.doRequirePrimary = True 

68 config.sourceSelector.doFlags = True 

69 config.sourceSelector.doSignalToNoise = True 

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

71 config.update(**kwargs) 

72 

73 return self.subtractTask(config=config) 

74 

75 

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

77 subtractTask = subtractImages.AlardLuptonSubtractTask 

78 

79 def test_allowed_config_modes(self): 

80 """Verify the allowable modes for convolution. 

81 """ 

82 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

83 config.mode = 'auto' 

84 config.mode = 'convolveScience' 

85 config.mode = 'convolveTemplate' 

86 

87 with self.assertRaises(FieldValidationError): 

88 config.mode = 'aotu' 

89 

90 def test_mismatched_template(self): 

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

92 does not fully contain the science image. 

93 """ 

94 xSize = 200 

95 ySize = 200 

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

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

98 task = self._setup_subtraction() 

99 with self.assertRaises(AssertionError): 

100 task.run(template, science, sources) 

101 

102 def test_mismatched_filter(self): 

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

104 different bands. 

105 """ 

106 xSize = 200 

107 ySize = 200 

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

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

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

111 band="not-g", physicalFilter="not-g noCamera") 

112 task = self._setup_subtraction() 

113 with self.assertRaises(AssertionError): 

114 task.run(template, science, sources) 

115 

116 def test_incomplete_template_coverage(self): 

117 noiseLevel = 1. 

118 border = 20 

119 xSize = 400 

120 ySize = 400 

121 nSources = 80 

122 science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, nSrc=nSources, 

123 xSize=xSize, ySize=ySize) 

124 template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, nSrc=nSources, 

125 templateBorderSize=border, doApplyCalibration=True, 

126 xSize=xSize, ySize=ySize) 

127 

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

129 

130 def _run_and_check_coverage(template_coverage, 

131 requiredTemplateFraction=0.1, 

132 minTemplateFractionForExpectedSuccess=0.2): 

133 template_cut = template.clone() 

134 template_height = int(science_height*template_coverage + border) 

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

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

137 task = self._setup_subtraction( 

138 requiredTemplateFraction=requiredTemplateFraction, 

139 minTemplateFractionForExpectedSuccess=minTemplateFractionForExpectedSuccess 

140 ) 

141 if template_coverage < requiredTemplateFraction: 

142 doRaise = True 

143 elif template_coverage < minTemplateFractionForExpectedSuccess: 

144 doRaise = True 

145 else: 

146 doRaise = False 

147 if doRaise: 

148 with self.assertRaises(NoWorkFound): 

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

150 else: 

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

152 _run_and_check_coverage(template_coverage=0.09) 

153 _run_and_check_coverage(template_coverage=0.19) 

154 _run_and_check_coverage(template_coverage=0.7) 

155 

156 def test_clear_template_mask(self): 

157 noiseLevel = 1. 

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

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

160 templateBorderSize=20, doApplyCalibration=True) 

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

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

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

164 mask = template.mask 

165 x0 = 50 

166 x1 = 75 

167 y0 = 150 

168 y1 = 175 

169 scienceMaskCheck = {} 

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

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

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

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

174 

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

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

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

178 if maskPlane in diffimEmptyMaskPlanes: 

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

180 elif maskPlane in task.config.preserveTemplateMask: 

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

182 else: 

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

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

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

186 diffimMask = output.difference.mask 

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

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

189 if maskPlane in diffimEmptyMaskPlanes: 

190 self.assertEqual(diffimSum, 0) 

191 else: 

192 self.assertTrue(diffimSum >= scienceSum) 

193 

194 def test_equal_images(self): 

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

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

197 """ 

198 noiseLevel = 1. 

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

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

201 templateBorderSize=20, doApplyCalibration=True) 

202 task = self._setup_subtraction() 

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

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

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

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

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

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

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

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

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

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

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

214 makeStats(), statistic=afwMath.STDEV) 

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

216 

217 def test_equal_images_missing_mask_planes(self): 

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

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

220 mask planes. 

221 """ 

222 noiseLevel = 1. 

223 science, sources = makeTestImage(psfSize=2.4, noiseLevel=noiseLevel, noiseSeed=6, addMaskPlanes=[]) 

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

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

226 task = self._setup_subtraction() 

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

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

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

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

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

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

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

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

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

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

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

238 makeStats(), statistic=afwMath.STDEV) 

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

240 

241 def test_psf_size(self): 

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

243 fwhmExposureBuffer and fwhmExposureGrid parameters are set. 

244 """ 

245 noiseLevel = 1. 

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

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

248 templateBorderSize=20, doApplyCalibration=True) 

249 

250 schema = afwTable.ExposureTable.makeMinimalSchema() 

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

252 exposureCatalog = afwTable.ExposureCatalog(schema) 

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

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

255 

256 record = exposureCatalog.addNew() 

257 record.setPsf(psf) 

258 record.setWcs(template.wcs) 

259 record.setD(weightKey, 1.0) 

260 record.setBBox(template.getBBox()) 

261 

262 customPsf = CustomCoaddPsf(exposureCatalog, template.wcs) 

263 template.setPsf(customPsf) 

264 

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

266 with self.assertRaises(InvalidParameterError): 

267 getPsfFwhm(template.psf, True) 

268 

269 with self.assertRaises(InvalidParameterError): 

270 getPsfFwhm(template.psf, False) 

271 

272 # Test that evaluateMeanPsfFwhm runs successfully on the template. 

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

274 

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

276 # all points in the science image. 

277 fwhm1 = getPsfFwhm(science.psf, False) 

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

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

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

281 

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

283 fwhmExposureGrid=10), 

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

285 ) 

286 

287 # Test that the image subtraction task runs successfully. 

288 task = self._setup_subtraction() 

289 

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

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

292 task.run(template, science, sources) 

293 

294 # Check that evaluateMeanPsfFwhm was called. 

295 # This tests that getPsfFwhm failed raising InvalidParameterError, 

296 # that is caught and handled appropriately. 

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

298 "Evaluting PSF on a grid of points." 

299 ) 

300 self.assertIn(logMessage, cm.output) 

301 

302 def test_auto_convolveTemplate(self): 

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

304 the template psf is the smaller. 

305 """ 

306 noiseLevel = 1. 

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

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

309 templateBorderSize=20, doApplyCalibration=True) 

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

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

312 

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

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

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

316 

317 def test_auto_convolveScience(self): 

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

319 the science psf is the smaller. 

320 """ 

321 noiseLevel = 1. 

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

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

324 templateBorderSize=20, doApplyCalibration=True) 

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

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

327 

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

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

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

331 

332 def test_science_better(self): 

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

334 with the science psf being smaller than the template. 

335 """ 

336 statsCtrl = makeStats() 

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

338 

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

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

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

342 templateBorderSize=20, doApplyCalibration=True) 

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

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

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

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

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

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

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

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

351 statsCtrlDetect) 

352 

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

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

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

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

357 statsCtrl) 

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

359 statsCtrl, statistic=afwMath.STDEV) 

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

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

362 

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

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

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

366 

367 def test_template_better(self): 

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

369 with the template psf being smaller than the science. 

370 """ 

371 statsCtrl = makeStats() 

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

373 

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

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

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

377 templateBorderSize=20, doApplyCalibration=True) 

378 task = self._setup_subtraction() 

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

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

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

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

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

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

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

386 

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

388 statsCtrlDetect) 

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

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

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

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

393 statsCtrl) 

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

395 statsCtrl, statistic=afwMath.STDEV) 

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

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

398 

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

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

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

402 

403 def test_symmetry(self): 

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

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

406 should be nearly the same. 

407 """ 

408 noiseLevel = 1. 

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

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

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

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

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

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

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

416 

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

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

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

420 

421 delta = template_better.difference.clone() 

422 delta.image -= science_better.difference.image 

423 delta.variance -= science_better.difference.variance 

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

425 

426 statsCtrl = makeStats() 

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

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

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

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

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

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

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

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

435 

436 def test_few_sources(self): 

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

438 """ 

439 xSize = 256 

440 ySize = 256 

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

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

443 task = self._setup_subtraction() 

444 sources = sources[0:1] 

445 with self.assertRaises(InsufficientKernelSourcesError): 

446 task.run(template, science, sources) 

447 

448 def test_kernel_source_selector(self): 

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

450 """ 

451 xSize = 256 

452 ySize = 256 

453 nSourcesSimulated = 20 

454 sciencePsfSize = 2.4 

455 templatePsfSize = 2.0 

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

457 xSize=xSize, ySize=ySize) 

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

459 xSize=xSize, ySize=ySize, doApplyCalibration=True) 

460 

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

462 sources = sourcesIn.copy(deep=True) 

463 

464 task = self._setup_subtraction(maxKernelSources=maxKernelSources, 

465 minKernelSources=minKernelSources, 

466 ) 

467 task.templatePsfSize = templatePsfSize 

468 task.sciencePsfSize = sciencePsfSize 

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

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

471 # the test. 

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

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

474 nSources = len(sources) 

475 # Flag a third of the sources 

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

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

478 bbox = science.getBBox() 

479 bbox.grow(-rejectRadius) 

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

481 sources[edgeSources][badSourceFlag] = True 

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

483 if maxKernelSources > 0: 

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

485 else: 

486 nGoodSources = nSources - nBadSources 

487 

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

489 signalToNoise = signalToNoise[~sources[badSourceFlag]] 

490 signalToNoise.sort() 

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

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

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

494 signalToNoiseOut.sort() 

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

496 

497 _run_and_check_sources(sources) 

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

499 _run_and_check_sources(sources, maxKernelSources=-1) 

500 with self.assertRaises(RuntimeError): 

501 _run_and_check_sources(sources, minKernelSources=1000) 

502 

503 def test_order_equal_images(self): 

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

505 if the images are equivalent. 

506 """ 

507 noiseLevel = .1 

508 seed1 = 6 

509 seed2 = 7 

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

511 clearEdgeMask=True) 

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

513 templateBorderSize=0, doApplyCalibration=True, 

514 clearEdgeMask=True) 

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

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

517 

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

519 clearEdgeMask=True) 

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

521 templateBorderSize=0, doApplyCalibration=True, 

522 clearEdgeMask=True) 

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

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

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

526 results_convolveScience.difference.getBBox()) 

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

528 diff1 -= template1.maskedImage[bbox] 

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

530 diff2 -= template2.maskedImage[bbox] 

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

532 diff1.image.array, 

533 atol=noiseLevel*5.) 

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

535 diff2.image.array, 

536 atol=noiseLevel*5.) 

537 diffErr = noiseLevel*2 

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

539 results_convolveScience.difference[bbox].maskedImage, 

540 atol=diffErr*5.) 

541 

542 def test_background_subtraction(self): 

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

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

545 

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

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

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

549 stay to make sure it continues functioning as intended. 

550 """ 

551 noiseLevel = 1. 

552 xSize = 512 

553 ySize = 512 

554 x0 = 123 

555 y0 = 456 

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

557 templateBorderSize=20, 

558 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

559 doApplyCalibration=True) 

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

561 

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

563 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

565 background=background_model, 

566 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

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

568 # Modifying the config of a subtask is messy. 

569 config = subtractImages.AlardLuptonSubtractTask.ConfigClass() 

570 

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

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

573 config.doSubtractBackground = True 

574 

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

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

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

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

579 statsCtrl = makeStats() 

580 

581 def _run_and_check_images(config, statsCtrl, mode): 

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

583 """ 

584 config.mode = mode 

585 task = subtractImages.AlardLuptonSubtractTask(config=config) 

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

587 

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

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

590 

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

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

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

594 

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

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

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

598 statsCtrl, statistic=afwMath.STDEV) 

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

600 

601 _run_and_check_images(config, statsCtrl, "convolveTemplate") 

602 _run_and_check_images(config, statsCtrl, "convolveScience") 

603 

604 def test_scale_variance_convolve_template(self): 

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

606 """ 

607 scienceNoiseLevel = 4. 

608 templateNoiseLevel = 2. 

609 scaleFactor = 1.345 

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

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

612 

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

614 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

617 """ 

618 

619 task = self._setup_subtraction(doDecorrelation=doDecorrelation, 

620 doScaleVariance=doScaleVariance, 

621 ) 

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

623 if doScaleVariance: 

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

625 scaleFactor, atol=0.05) 

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

627 scaleFactor, atol=0.05) 

628 

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

630 if doDecorrelation: 

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

632 else: 

633 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

634 output.matchedTemplate.mask, 

635 statsCtrl) 

636 

637 if doScaleVariance: 

638 templateNoise *= scaleFactor 

639 scienceNoise *= scaleFactor 

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

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

642 

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

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

645 templateBorderSize=20, doApplyCalibration=True) 

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

647 # when the template and science variance planes are correct 

648 _run_and_check_images(science, template, sources, statsCtrl, 

649 doDecorrelation=True, doScaleVariance=True) 

650 _run_and_check_images(science, template, sources, statsCtrl, 

651 doDecorrelation=True, doScaleVariance=False) 

652 _run_and_check_images(science, template, sources, statsCtrl, 

653 doDecorrelation=False, doScaleVariance=True) 

654 _run_and_check_images(science, template, sources, statsCtrl, 

655 doDecorrelation=False, doScaleVariance=False) 

656 

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

658 # when the template variance plane is incorrect 

659 template.variance.array /= scaleFactor 

660 science.variance.array /= scaleFactor 

661 _run_and_check_images(science, template, sources, statsCtrl, 

662 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

663 _run_and_check_images(science, template, sources, statsCtrl, 

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

665 _run_and_check_images(science, template, sources, statsCtrl, 

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

667 _run_and_check_images(science, template, sources, statsCtrl, 

668 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

669 

670 def test_scale_variance_convolve_science(self): 

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

672 """ 

673 scienceNoiseLevel = 4. 

674 templateNoiseLevel = 2. 

675 scaleFactor = 1.345 

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

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

678 

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

680 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

683 """ 

684 

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

686 doDecorrelation=doDecorrelation, 

687 doScaleVariance=doScaleVariance, 

688 restrictKernelEdgeSources=False, 

689 ) 

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

691 if doScaleVariance: 

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

693 scaleFactor, atol=0.05) 

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

695 scaleFactor, atol=0.05) 

696 

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

698 if doDecorrelation: 

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

700 else: 

701 scienceNoise = computeRobustStatistics(output.matchedScience.variance, 

702 output.matchedScience.mask, 

703 statsCtrl) 

704 

705 if doScaleVariance: 

706 templateNoise *= scaleFactor 

707 scienceNoise *= scaleFactor 

708 

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

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

711 

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

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

714 templateBorderSize=20, doApplyCalibration=True) 

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

716 # when the template and science variance planes are correct 

717 _run_and_check_images(science, template, sources, statsCtrl, 

718 doDecorrelation=True, doScaleVariance=True) 

719 _run_and_check_images(science, template, sources, statsCtrl, 

720 doDecorrelation=True, doScaleVariance=False) 

721 _run_and_check_images(science, template, sources, statsCtrl, 

722 doDecorrelation=False, doScaleVariance=True) 

723 _run_and_check_images(science, template, sources, statsCtrl, 

724 doDecorrelation=False, doScaleVariance=False) 

725 

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

727 # when the template and science variance planes are incorrect 

728 science.variance.array /= scaleFactor 

729 template.variance.array /= scaleFactor 

730 _run_and_check_images(science, template, sources, statsCtrl, 

731 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

732 _run_and_check_images(science, template, sources, statsCtrl, 

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

734 _run_and_check_images(science, template, sources, statsCtrl, 

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

736 _run_and_check_images(science, template, sources, statsCtrl, 

737 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

738 

739 def test_exposure_properties_convolve_template(self): 

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

741 when the template is convolved. 

742 """ 

743 noiseLevel = 1. 

744 seed = 37 

745 rng = np.random.RandomState(seed) 

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

747 psf = science.psf 

748 psfAvgPos = psf.getAveragePosition() 

749 psfSize = getPsfFwhm(science.psf) 

750 psfImg = psf.computeKernelImage(psfAvgPos) 

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

752 templateBorderSize=20, doApplyCalibration=True) 

753 

754 # Generate a random aperture correction map 

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

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

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

758 science.info.setApCorrMap(apCorrMap) 

759 

760 def _run_and_check_images(doDecorrelation): 

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

762 """ 

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

764 doDecorrelation=doDecorrelation, 

765 ) 

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

767 psfOut = output.difference.psf 

768 psfAvgPos = psfOut.getAveragePosition() 

769 if doDecorrelation: 

770 # Decorrelation requires recalculating the PSF, 

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

772 psfOutSize = getPsfFwhm(science.psf) 

773 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

774 else: 

775 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

776 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

777 

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

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

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

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

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

783 _run_and_check_images(doDecorrelation=True) 

784 _run_and_check_images(doDecorrelation=False) 

785 

786 def test_exposure_properties_convolve_science(self): 

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

788 when the science image is convolved. 

789 """ 

790 noiseLevel = 1. 

791 seed = 37 

792 rng = np.random.RandomState(seed) 

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

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

795 templateBorderSize=20, doApplyCalibration=True) 

796 psf = template.psf 

797 psfAvgPos = psf.getAveragePosition() 

798 psfSize = getPsfFwhm(template.psf) 

799 psfImg = psf.computeKernelImage(psfAvgPos) 

800 

801 # Generate a random aperture correction map 

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

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

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

805 science.info.setApCorrMap(apCorrMap) 

806 

807 def _run_and_check_images(doDecorrelation): 

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

809 """ 

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

811 doDecorrelation=doDecorrelation, 

812 ) 

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

814 if doDecorrelation: 

815 # Decorrelation requires recalculating the PSF, 

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

817 psfOutSize = getPsfFwhm(template.psf) 

818 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

819 else: 

820 psfOut = output.difference.psf 

821 psfAvgPos = psfOut.getAveragePosition() 

822 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

823 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

824 

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

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

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

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

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

830 

831 _run_and_check_images(doDecorrelation=True) 

832 _run_and_check_images(doDecorrelation=False) 

833 

834 def _compare_apCorrMaps(self, a, b): 

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

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

837 

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

839 

840 Parameters 

841 ---------- 

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

843 The two aperture correction maps to compare. 

844 """ 

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

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

847 value2 = b.get(name) 

848 self.assertIsNotNone(value2) 

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

850 self.assertFloatsAlmostEqual( 

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

852 

853 def test_fake_mask_plane_propagation(self): 

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

855 This is testing method called updateMasks 

856 """ 

857 xSize = 200 

858 ySize = 200 

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

860 science_fake_img, science_fake_sources = makeTestImage( 

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

862 ) 

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

864 tmplt_fake_img, tmplt_fake_sources = makeTestImage( 

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

866 ) 

867 # created fakes and added them to the images 

868 science.image.array += science_fake_img.image.array 

869 template.image.array += tmplt_fake_img.image.array 

870 

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

872 # adding mask planes to both science and template images 

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

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

875 

876 for a_science_source in science_fake_sources: 

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

878 bbox = lsst.geom.Box2I( 

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

880 ) 

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

882 

883 for a_template_source in tmplt_fake_sources: 

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

885 bbox = lsst.geom.Box2I( 

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

887 lsst.geom.Extent2I(3, 3) 

888 ) 

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

890 

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

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

893 

894 task = self._setup_subtraction() 

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

896 

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

898 diff_mask = subtraction.difference.mask 

899 

900 # science mask should be now in INJECTED 

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

902 

903 # template mask should be now in INJECTED_TEMPLATE 

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

905 

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

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

908 

909 def test_metadata_metrics(self): 

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

911 that the difference image limiting magnitude is calculated correctly, 

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

913 """ 

914 science, sources = makeTestImage(psfSize=2.8, noiseLevel=1) 

915 template_good, _ = makeTestImage(psfSize=2.4, doApplyCalibration=True, noiseLevel=0.25, 

916 templateBorderSize=20) 

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

918 templateBorderSize=20) 

919 

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

921 config = measAlg.SkyObjectsTask.ConfigClass() 

922 config.nSources = 3 

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

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

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

926 sources = sources.copy(deep=True) 

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

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

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

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

931 

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

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

934 

935 subtractTask_good = self._setup_subtraction() 

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

937 subtractTask_bad = self._setup_subtraction() 

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

939 

940 # Test that the diffim limiting magnitudes are computed correctly 

941 maglim_science = subtractTask_good._calculateMagLim(science) 

942 fluxlim_science = (maglim_science*u.ABmag).to_value(u.nJy) 

943 maglim_template_good = subtractTask_good._calculateMagLim(template_good) 

944 fluxlim_template_good = (maglim_template_good*u.ABmag).to_value(u.nJy) 

945 maglim_template_bad = subtractTask_bad._calculateMagLim(template_bad) 

946 fluxlim_template_bad = (maglim_template_bad*u.ABmag).to_value(u.nJy) 

947 

948 maglim_good = (np.sqrt(fluxlim_science**2 + fluxlim_template_good**2)*u.nJy).to(u.ABmag).value 

949 maglim_bad = (np.sqrt(fluxlim_science**2 + fluxlim_template_bad**2)*u.nJy).to(u.ABmag).value 

950 

951 self.assertFloatsAlmostEqual(subtractTask_good.metadata['diffimLimitingMagnitude'], 

952 maglim_good, atol=1e-6) 

953 self.assertFloatsAlmostEqual(subtractTask_bad.metadata['diffimLimitingMagnitude'], 

954 maglim_bad, atol=1e-6) 

955 

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

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

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

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

960 template_offimage, _ = makeTestImage() 

961 schema = afwTable.ExposureTable.makeMinimalSchema() 

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

963 exposureCatalog = afwTable.ExposureCatalog(schema) 

964 record = exposureCatalog.addNew() 

965 record.setD(weightKey, 1.0) 

966 record.setBBox(template_offimage.getBBox()) 

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

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

969 record.setPsf(psf) 

970 record.setWcs(template_offimage.wcs) 

971 custom_offimage_psf = CustomCoaddPsf(exposureCatalog, template_offimage.wcs) 

972 template_offimage.setPsf(custom_offimage_psf) 

973 

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

975 subtractTask_offimage = self._setup_subtraction() 

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

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

978 # limiting magnitude. 

979 maglim_template_offimage = subtractTask_offimage._calculateMagLim(template_offimage) 

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

981 # Test that given the provided fallbackPsfSize, the diffim limiting 

982 # magnitude is calculated correctly. 

983 maglim_template_offimage = 28.182284789714952 

984 fluxlim_template_offimage = (maglim_template_offimage*u.ABmag).to_value(u.nJy) 

985 maglim_offimage = (np.sqrt(fluxlim_science**2 + fluxlim_template_offimage**2)*u.nJy).to(u.ABmag).value 

986 self.assertEqual(subtractTask_offimage.metadata['diffimLimitingMagnitude'], maglim_offimage) 

987 

988 # Test that several other expected metadata metrics exist 

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

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

991 

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

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

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

995 

996 

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

998 subtractTask = subtractImages.AlardLuptonPreconvolveSubtractTask 

999 

1000 def test_mismatched_template(self): 

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

1002 does not fully contain the science image. 

1003 """ 

1004 xSize = 200 

1005 ySize = 200 

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

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

1008 task = self._setup_subtraction() 

1009 with self.assertRaises(AssertionError): 

1010 task.run(template, science, sources) 

1011 

1012 def test_equal_images(self): 

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

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

1015 """ 

1016 noiseLevel = 1. 

1017 xSize = 400 

1018 ySize = 400 

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

1020 xSize=xSize, ySize=ySize) 

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

1022 templateBorderSize=20, doApplyCalibration=True, 

1023 xSize=xSize, ySize=ySize) 

1024 task = self._setup_subtraction() 

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

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

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

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

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

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

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

1032 scoreMean = computeRobustStatistics(output.scoreExposure.image, 

1033 output.scoreExposure.mask, 

1034 statsCtrl) 

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

1036 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

1039 statsCtrl=statsCtrl, statistic=afwMath.STDEV) 

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

1041 

1042 def test_incomplete_template_coverage(self): 

1043 noiseLevel = 1. 

1044 border = 20 

1045 xSize = 400 

1046 ySize = 400 

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

1048 xSize=xSize, ySize=ySize) 

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

1050 templateBorderSize=border, doApplyCalibration=True, 

1051 xSize=xSize, ySize=ySize) 

1052 

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

1054 

1055 def _run_and_check_coverage(template_coverage): 

1056 template_cut = template.clone() 

1057 template_height = int(science_height*template_coverage + border) 

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

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

1060 task = self._setup_subtraction() 

1061 if template_coverage < task.config.requiredTemplateFraction: 

1062 doRaise = True 

1063 elif template_coverage < task.config.minTemplateFractionForExpectedSuccess: 

1064 doRaise = True 

1065 else: 

1066 doRaise = False 

1067 if doRaise: 

1068 with self.assertRaises(NoWorkFound): 

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

1070 else: 

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

1072 _run_and_check_coverage(template_coverage=0.09) 

1073 _run_and_check_coverage(template_coverage=0.19) 

1074 _run_and_check_coverage(template_coverage=.7) 

1075 

1076 def test_clear_template_mask(self): 

1077 noiseLevel = 1. 

1078 xSize = 400 

1079 ySize = 400 

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

1081 xSize=xSize, ySize=ySize) 

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

1083 templateBorderSize=20, doApplyCalibration=True, 

1084 xSize=xSize, ySize=ySize) 

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

1086 task = self._setup_subtraction() 

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

1088 mask = template.mask 

1089 x0 = 50 

1090 x1 = 75 

1091 y0 = 150 

1092 y1 = 175 

1093 scienceMaskCheck = {} 

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

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

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

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

1098 

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

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

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

1102 if maskPlane in diffimEmptyMaskPlanes: 

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

1104 elif maskPlane in task.config.preserveTemplateMask: 

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

1106 else: 

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

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

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

1110 diffimMask = output.scoreExposure.mask 

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

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

1113 if maskPlane in diffimEmptyMaskPlanes: 

1114 self.assertEqual(diffimSum, 0) 

1115 else: 

1116 self.assertTrue(diffimSum >= scienceSum) 

1117 

1118 def test_agnostic_template_psf(self): 

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

1120 larger or smaller than the science image PSF. 

1121 """ 

1122 noiseLevel = .3 

1123 xSize = 400 

1124 ySize = 400 

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

1126 noiseSeed=6, templateBorderSize=0, 

1127 xSize=xSize, ySize=ySize) 

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

1129 noiseSeed=7, doApplyCalibration=True, 

1130 xSize=xSize, ySize=ySize) 

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

1132 noiseSeed=8, doApplyCalibration=True, 

1133 xSize=xSize, ySize=ySize) 

1134 task = self._setup_subtraction() 

1135 

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

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

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

1139 

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

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

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

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

1144 

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

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

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

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

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

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

1151 statistic=afwMath.STDEV) 

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

1153 nea = computePSFNoiseEquivalentArea(science.psf) 

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

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

1156 

1157 def test_few_sources(self): 

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

1159 """ 

1160 xSize = 256 

1161 ySize = 256 

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

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

1164 task = self._setup_subtraction() 

1165 sources = sources[0:1] 

1166 with self.assertRaises(InsufficientKernelSourcesError): 

1167 task.run(template, science, sources) 

1168 

1169 def test_background_subtraction(self): 

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

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

1172 """ 

1173 noiseLevel = 1. 

1174 xSize = 512 

1175 ySize = 512 

1176 x0 = 123 

1177 y0 = 456 

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

1179 templateBorderSize=20, 

1180 xSize=xSize, ySize=ySize, x0=x0, y0=y0, 

1181 doApplyCalibration=True) 

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

1183 

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

1185 background_model = afwMath.Chebyshev1Function2D(params, bbox2D) 

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

1187 background=background_model, 

1188 xSize=xSize, ySize=ySize, x0=x0, y0=y0) 

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

1190 # Modifying the config of a subtask is messy. 

1191 config = subtractImages.AlardLuptonPreconvolveSubtractTask.ConfigClass() 

1192 

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

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

1195 config.doSubtractBackground = True 

1196 

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

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

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

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

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

1202 

1203 task = subtractImages.AlardLuptonPreconvolveSubtractTask(config=config) 

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

1205 

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

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

1208 

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

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

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

1212 

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

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

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

1216 statsCtrl, statistic=afwMath.STDEV) 

1217 # get the img psf Noise Equivalent Area value 

1218 nea = computePSFNoiseEquivalentArea(science.psf) 

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

1220 

1221 def test_scale_variance(self): 

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

1223 """ 

1224 scienceNoiseLevel = 4. 

1225 templateNoiseLevel = 2. 

1226 scaleFactor = 1.345 

1227 xSize = 400 

1228 ySize = 400 

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

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

1231 

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

1233 doDecorrelation, doScaleVariance, scaleFactor=1.): 

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

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

1236 """ 

1237 

1238 task = self._setup_subtraction(doDecorrelation=doDecorrelation, 

1239 doScaleVariance=doScaleVariance, 

1240 ) 

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

1242 if doScaleVariance: 

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

1244 scaleFactor, atol=0.05) 

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

1246 scaleFactor, atol=0.05) 

1247 

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

1249 # get the img psf Noise Equivalent Area value 

1250 nea = computePSFNoiseEquivalentArea(science.psf) 

1251 scienceNoise /= nea 

1252 if doDecorrelation: 

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

1254 templateNoise /= nea 

1255 else: 

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

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

1258 templateNoise = computeRobustStatistics(output.matchedTemplate.variance, 

1259 output.matchedTemplate.mask, 

1260 statsCtrl) 

1261 if doScaleVariance: 

1262 templateNoise *= scaleFactor 

1263 scienceNoise *= scaleFactor 

1264 varMean = computeRobustStatistics(output.scoreExposure.variance, 

1265 output.scoreExposure.mask, 

1266 statsCtrl) 

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

1268 

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

1270 xSize=xSize, ySize=ySize) 

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

1272 templateBorderSize=20, doApplyCalibration=True, 

1273 xSize=xSize, ySize=ySize) 

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

1275 # when the template and science variance planes are correct 

1276 _run_and_check_images(science, template, sources, statsCtrl, 

1277 doDecorrelation=True, doScaleVariance=True) 

1278 _run_and_check_images(science, template, sources, statsCtrl, 

1279 doDecorrelation=True, doScaleVariance=False) 

1280 _run_and_check_images(science, template, sources, statsCtrl, 

1281 doDecorrelation=False, doScaleVariance=True) 

1282 _run_and_check_images(science, template, sources, statsCtrl, 

1283 doDecorrelation=False, doScaleVariance=False) 

1284 

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

1286 # when the template variance plane is incorrect 

1287 template.variance.array /= scaleFactor 

1288 science.variance.array /= scaleFactor 

1289 _run_and_check_images(science, template, sources, statsCtrl, 

1290 doDecorrelation=True, doScaleVariance=True, scaleFactor=scaleFactor) 

1291 _run_and_check_images(science, template, sources, statsCtrl, 

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

1293 _run_and_check_images(science, template, sources, statsCtrl, 

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

1295 _run_and_check_images(science, template, sources, statsCtrl, 

1296 doDecorrelation=False, doScaleVariance=False, scaleFactor=scaleFactor) 

1297 

1298 def test_exposure_properties(self): 

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

1300 with the Score image. 

1301 """ 

1302 noiseLevel = 1. 

1303 xSize = 400 

1304 ySize = 400 

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

1306 xSize=xSize, ySize=ySize) 

1307 psf = science.psf 

1308 psfAvgPos = psf.getAveragePosition() 

1309 psfSize = getPsfFwhm(science.psf) 

1310 psfImg = psf.computeKernelImage(psfAvgPos) 

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

1312 templateBorderSize=20, doApplyCalibration=True, 

1313 xSize=xSize, ySize=ySize) 

1314 

1315 def _run_and_check_images(doDecorrelation): 

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

1317 """ 

1318 task = self._setup_subtraction(doDecorrelation=doDecorrelation) 

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

1320 psfOut = output.scoreExposure.psf 

1321 psfAvgPos = psfOut.getAveragePosition() 

1322 if doDecorrelation: 

1323 # Decorrelation requires recalculating the PSF, 

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

1325 psfOutSize = getPsfFwhm(science.psf) 

1326 self.assertFloatsAlmostEqual(psfSize, psfOutSize) 

1327 else: 

1328 psfOutImg = psfOut.computeKernelImage(psfAvgPos) 

1329 self.assertImagesAlmostEqual(psfImg, psfOutImg) 

1330 

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

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

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

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

1335 _run_and_check_images(doDecorrelation=True) 

1336 _run_and_check_images(doDecorrelation=False) 

1337 

1338 

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

1340 subtractTask = subtractImages.SimplifiedSubtractTask 

1341 

1342 def test_runSimplifiedTaskWithExistingKernel(self): 

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

1344 `AlardLuptonSubtractTask` if it uses the AL kernel. 

1345 """ 

1346 noiseLevel = 1. 

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

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

1349 templateBorderSize=20, doApplyCalibration=True) 

1350 alTask = AlardLuptonSubtractTest._setup_subtraction(AlardLuptonSubtractTest()) 

1351 task = self._setup_subtraction(useExistingKernel=True) 

1352 

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

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

1355 inputPsfMatchingKernel=alResults.psfMatchingKernel) 

1356 

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

1358 

1359 def test_runSimplifiedTaskWithSourceDetection(self): 

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

1361 reasonable output. 

1362 """ 

1363 noiseLevel = 1. 

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

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

1366 templateBorderSize=20, doApplyCalibration=True) 

1367 task = self._setup_subtraction(useExistingKernel=False, 

1368 fluxField="base_PsfFlux_instFlux", 

1369 errField="base_PsfFlux_instFluxErr", 

1370 ) 

1371 

1372 output = task.run(template, science) 

1373 

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

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

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

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

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

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

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

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

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

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

1384 makeStats(), statistic=afwMath.STDEV) 

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

1386 

1387 

1388def setup_module(module): 

1389 lsst.utils.tests.init() 

1390 

1391 

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

1393 pass 

1394 

1395 

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

1397 lsst.utils.tests.init() 

1398 unittest.main()