Coverage for tests/test_imageDifference.py: 14%

286 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-26 11:13 +0000

1# This file is part of ip_diffim. 

2# 

3# LSST Data Management System 

4# This product includes software developed by the 

5# LSST Project (http://www.lsst.org/). 

6# See COPYRIGHT file at the top of the source tree. 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23import numpy as np 

24 

25import lsst.afw.geom as afwGeom 

26import lsst.afw.math as afwMath 

27import lsst.daf.base as dafBase 

28import lsst.geom as geom 

29from lsst.meas.algorithms.testUtils import plantSources 

30import lsst.utils.tests 

31 

32from lsst.ip.diffim.imageDecorrelation import DecorrelateALKernelTask, DecorrelateALKernelConfig 

33from lsst.ip.diffim.imagePsfMatch import ImagePsfMatchTask, ImagePsfMatchConfig 

34from lsst.ip.diffim.zogy import ZogyTask, ZogyConfig 

35 

36 

37class ImageDifferenceTestBase(lsst.utils.tests.TestCase): 

38 """A test case for comparing image differencing algorithms. 

39 

40 Attributes 

41 ---------- 

42 bbox : `lsst.afw.geom.Box2I` 

43 Bounding box of the test model. 

44 bufferSize : `int` 

45 Distance from the inner edge of the bounding box 

46 to avoid placing test sources in the model images. 

47 nRandIter : `int` 

48 Number of iterations to repeat each test with random numbers. 

49 statsCtrl : `lsst.afw.math.StatisticsControl` 

50 Statistics control object. 

51 """ 

52 

53 def setUp(self): 

54 """Define the filter, DCR parameters, and the bounding box for the tests. 

55 """ 

56 self.nRandIter = 5 # Number of iterations to repeat each test with random numbers. 

57 self.bufferSize = 5 

58 xSize = 250 

59 ySize = 260 

60 x0 = 12345 

61 y0 = 67890 

62 self.bbox = geom.Box2I(geom.Point2I(x0, y0), geom.Extent2I(xSize, ySize)) 

63 self.statsCtrl = afwMath.StatisticsControl() 

64 self.statsCtrl.setNumSigmaClip(3.) 

65 self.statsCtrl.setNumIter(3) 

66 

67 def makeTestImages(self, seed=5, nSrc=5, psfSize=2., noiseLevel=5., 

68 fluxLevel=500., fluxRange=2.): 

69 """Make reproduceable PSF-convolved masked images for testing. 

70 

71 Parameters 

72 ---------- 

73 seed : `int`, optional 

74 Seed value to initialize the random number generator. 

75 nSrc : `int`, optional 

76 Number of sources to simulate. 

77 psfSize : `float`, optional 

78 Width of the PSF of the simulated sources, in pixels. 

79 noiseLevel : `float`, optional 

80 Standard deviation of the noise to add to each pixel. 

81 fluxLevel : `float`, optional 

82 Reference flux of the simulated sources. 

83 fluxRange : `float`, optional 

84 Range in flux amplitude of the simulated sources. 

85 

86 Returns 

87 ------- 

88 modelImages : `lsst.afw.image.ExposureF` 

89 The model image, with the mask and variance planes. 

90 sourceCat : `lsst.afw.table.SourceCatalog` 

91 Catalog of sources detected on the model image. 

92 """ 

93 rng = np.random.RandomState(seed) 

94 x0, y0 = self.bbox.getBegin() 

95 xSize, ySize = self.bbox.getDimensions() 

96 xLoc = rng.rand(nSrc)*(xSize - 2*self.bufferSize) + self.bufferSize + x0 

97 yLoc = rng.rand(nSrc)*(ySize - 2*self.bufferSize) + self.bufferSize + y0 

98 

99 flux = (rng.rand(nSrc)*(fluxRange - 1.) + 1.)*fluxLevel 

100 sigmas = [psfSize for src in range(nSrc)] 

101 coordList = list(zip(xLoc, yLoc, flux, sigmas)) 

102 kernelSize = int(xSize/2) # Need a careful explanation of this kernel size choice 

103 skyLevel = 0 

104 # Don't use the built in poisson noise: it modifies the global state of numpy random 

105 model = plantSources(self.bbox, kernelSize, skyLevel, coordList, addPoissonNoise=False) 

106 noise = rng.rand(ySize, xSize)*noiseLevel 

107 model.image.array += noise 

108 model.variance.array = (np.sqrt(np.abs(model.image.array)) + noiseLevel 

109 - np.mean(np.sqrt(np.abs(noise)))) 

110 

111 # Run source detection to set up the mask plane 

112 psfMatchTask = ImagePsfMatchTask(config=ImagePsfMatchConfig()) 

113 sourceCat = psfMatchTask.getSelectSources(model) 

114 

115 model.setWcs(self._makeWcs()) 

116 return model, sourceCat 

117 

118 @staticmethod 

119 def _makeWcs(offset=0): 

120 """Make a fake Wcs. 

121 

122 Parameters 

123 ---------- 

124 offset : `float` 

125 offset the Wcs by this many pixels. 

126 """ 

127 # taken from $AFW_DIR/tests/testMakeWcs.py 

128 metadata = dafBase.PropertySet() 

129 metadata.set("SIMPLE", "T") 

130 metadata.set("BITPIX", -32) 

131 metadata.set("NAXIS", 2) 

132 metadata.set("NAXIS1", 1024) 

133 metadata.set("NAXIS2", 1153) 

134 metadata.set("RADESYS", 'FK5') 

135 metadata.set("EQUINOX", 2000.) 

136 metadata.setDouble("CRVAL1", 215.604025685476) 

137 metadata.setDouble("CRVAL2", 53.1595451514076) 

138 metadata.setDouble("CRPIX1", 1109.99981456774 + offset) 

139 metadata.setDouble("CRPIX2", 560.018167811613 + offset) 

140 metadata.set("CTYPE1", 'RA---SIN') 

141 metadata.set("CTYPE2", 'DEC--SIN') 

142 metadata.setDouble("CD1_1", 5.10808596133527E-05) 

143 metadata.setDouble("CD1_2", 1.85579539217196E-07) 

144 metadata.setDouble("CD2_2", -5.10281493481982E-05) 

145 metadata.setDouble("CD2_1", -8.27440751733828E-07) 

146 return afwGeom.makeSkyWcs(metadata) 

147 

148 def diffimMetricBasic(self, residual, sourceCat, radius=2, sigma=0.): 

149 """Compute a basic metric based on the total number of positive and 

150 negative pixels in a residual image. 

151 

152 Parameters 

153 ---------- 

154 residual : `lsst.afw.image.ExposureF` 

155 A residual image resulting from image differencing. 

156 sourceCat : `lsst.afw.table.SourceCatalog` 

157 Source catalog containing the locations to calculate the metric. 

158 radius : `int`, optional 

159 Radius in pixels to use around each source location for the metric. 

160 sigma : `float`, optional 

161 Threshold to include pixel values in the metric. 

162 

163 Returns 

164 ------- 

165 `float` 

166 Metric assessing the image differencing residual. 

167 """ 

168 nNeg = 0 

169 nPos = 0 

170 threshold = sigma*self.computeExposureStddev(residual) 

171 for src in sourceCat: 

172 srcX = int(src.getX()) - residual.getBBox().getBeginX() 

173 srcY = int(src.getY()) - residual.getBBox().getBeginY() 

174 srcRes = residual.image.array[srcY - radius: srcY + radius + 1, srcX - radius: srcX + radius + 1] 

175 nPos += np.sum(srcRes > threshold) 

176 nNeg += np.sum(srcRes < -threshold) 

177 

178 if (nPos + nNeg) == 0: 

179 metric = 0. 

180 else: 

181 metric = (nPos - nNeg)/(nPos + nNeg) 

182 return metric 

183 

184 def computeExposureStddev(self, exposure): 

185 """Compute the standard deviation of an exposure, using the mask plane. 

186 

187 Parameters 

188 ---------- 

189 exposure : `lsst.afw.image.ExposureF` 

190 The input exposure. 

191 

192 Returns 

193 ------- 

194 `float` 

195 The standard deviation of the unmasked pixels of the input image. 

196 """ 

197 statObj = afwMath.makeStatistics(exposure.maskedImage.image, 

198 exposure.maskedImage.mask, 

199 afwMath.STDEVCLIP, self.statsCtrl) 

200 var = statObj.getValue(afwMath.STDEVCLIP) 

201 return var 

202 

203 @staticmethod 

204 def wrapZogyDiffim(config, templateExposure, scienceExposure): 

205 """Prepare and run ZOGY-style image differencing. 

206 

207 Parameters 

208 ---------- 

209 config : `lsst.pex.config.Config` 

210 The image differencing Task configuration settings. 

211 templateExposure : `lsst.afw.image.ExposureF` 

212 The reference image to subtract from the science image. 

213 scienceExposure : `lsst.afw.image.ExposureF` 

214 The science image. 

215 

216 Returns 

217 ------- 

218 `lsst.afw.image.ExposureF` 

219 The image difference. 

220 """ 

221 config.scaleByCalibration = False 

222 zogyTask = ZogyTask(config=config) 

223 

224 result = zogyTask.run(scienceExposure, templateExposure) 

225 return result.diffExp 

226 

227 @staticmethod 

228 def wrapAlDiffim(config, templateExposure, scienceExposure, convolveTemplate=True, returnKernel=False, 

229 precomputeKernelCandidates=False): 

230 """Prepare and run Alard&Lupton-style image differencing. 

231 

232 Parameters 

233 ---------- 

234 config : `lsst.pex.config.Config` 

235 The image differencing Task configuration settings. 

236 templateExposure : `lsst.afw.image.ExposureF` 

237 The reference image to subtract from the science image. 

238 scienceExposure : `lsst.afw.image.ExposureF` 

239 The science image. 

240 convolveTemplate : `bool`, optional 

241 Option to convolve the template or the science image. 

242 returnKernel : `bool`, optional 

243 Option to return the residual image or the matching kernel. 

244 

245 Returns 

246 ------- 

247 `lsst.afw.image.ExposureF` or `lsst.afw.math.LinearCombinationKernel` 

248 The image difference, or the PSF matching kernel. 

249 """ 

250 alTask = ImagePsfMatchTask(config=config) 

251 candidateList = None 

252 if precomputeKernelCandidates: 

253 if convolveTemplate: 

254 candidateList = alTask.getSelectSources(scienceExposure.clone()) 

255 else: 

256 candidateList = alTask.getSelectSources(templateExposure.clone()) 

257 templateFwhmPix = templateExposure.getPsf().getSigma() 

258 scienceFwhmPix = scienceExposure.getPsf().getSigma() 

259 result = alTask.subtractExposures(templateExposure, scienceExposure, 

260 templateFwhmPix=templateFwhmPix, 

261 scienceFwhmPix=scienceFwhmPix, 

262 doWarping=False, 

263 convolveTemplate=convolveTemplate, 

264 candidateList=candidateList, 

265 ) 

266 if returnKernel: 

267 return result.psfMatchingKernel 

268 else: 

269 return result.subtractedExposure 

270 

271 

272class ImageDifferenceTestVerification(ImageDifferenceTestBase): 

273 

274 def testModelImages(self): 

275 """Check that the simulated images are useable. 

276 """ 

277 sciPsf = 2.4 

278 refPsf = 2. 

279 sciNoise = 5. 

280 refNoise = 1.5 

281 fluxRatio = refPsf**2/sciPsf**2 

282 sciIm, src = self.makeTestImages(psfSize=sciPsf, noiseLevel=sciNoise) 

283 sciIm2, _ = self.makeTestImages(psfSize=sciPsf, noiseLevel=sciNoise) 

284 refIm, _ = self.makeTestImages(psfSize=refPsf, noiseLevel=refNoise) 

285 

286 # Making the test images should be repeatable 

287 self.assertFloatsAlmostEqual(sciIm.image.array, sciIm2.image.array) 

288 

289 diffIm = sciIm.clone() 

290 diffIm.image.array -= refIm.image.array 

291 

292 # The "reference" image has a smaller PSF but the same source fluxes, so the peak should be greater. 

293 self.assertGreater(np.max(refIm.image.array), np.max(sciIm.image.array)) 

294 # The difference image won't be zero since the two images have different PSFs, 

295 # but the peak should be much lower. 

296 sciPeak = np.max(sciIm.image.array) 

297 residualPeak = np.sqrt(1 - fluxRatio)*sciPeak 

298 self.assertGreater(residualPeak, np.max(abs(diffIm.image.array))) 

299 

300 # It should be possible to compute the diffim metric from the science and reference images 

301 refMetric = self.diffimMetricBasic(refIm, src, sigma=3) 

302 sciMetric = self.diffimMetricBasic(sciIm, src, sigma=3) 

303 self.assertGreaterEqual(refMetric, -1) 

304 self.assertGreaterEqual(sciMetric, -1) 

305 

306 def testSimDiffim(self): 

307 "Basic smoke test to verify that the test code itself can run." 

308 refPsf = 2.4 

309 sciPsfBase = 2. 

310 sciNoise = 5. 

311 refNoise = 1.5 

312 seed = 8 

313 fluxLevel = 500 

314 decorrelate = DecorrelateALKernelTask() 

315 zogyConfig = ZogyConfig() 

316 alConfig = ImagePsfMatchConfig() 

317 

318 for s in range(self.nRandIter): 

319 sciPsf = sciPsfBase + s*0.2 

320 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf, 

321 noiseLevel=refNoise, fluxLevel=fluxLevel) 

322 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf, 

323 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

324 # The diffim tasks might modify the images, 

325 # so make a deep copy to make sure they are independent 

326 sci2 = sci.clone() 

327 ref2 = ref.clone() 

328 

329 resAl = self.wrapAlDiffim(alConfig, ref, sci) 

330 resZogy = self.wrapZogyDiffim(zogyConfig, ref2, sci2) 

331 metricZogy = self.diffimMetricBasic(resZogy, src, sigma=3) 

332 metricAl = self.diffimMetricBasic(resAl, src, sigma=3) 

333 mKernel = self.wrapAlDiffim(alConfig, ref, sci, returnKernel=True) 

334 resDecorr = decorrelate.run(sci, ref, resAl, mKernel).correctedExposure 

335 metricDecorr = self.diffimMetricBasic(resDecorr, src, sigma=3) 

336 self.assertGreaterEqual(metricZogy, -1) 

337 self.assertGreaterEqual(metricAl, -1) 

338 self.assertGreaterEqual(metricDecorr, -1) 

339 

340 

341class ImageDifferenceTestAlardLupton(ImageDifferenceTestBase): 

342 

343 def testSimAlRefNotModified(self): 

344 "Image differencing should not modify the original template image." 

345 refPsf = 2. 

346 sciPsfBase = 2. 

347 sciNoise = 5. 

348 refNoise = 1.5 

349 seed = 37 

350 fluxLevel = 500 

351 rng = np.random.RandomState(seed) 

352 alConfig = ImagePsfMatchConfig() 

353 

354 sciPsf = sciPsfBase + rng.random()*2. 

355 refOriginal, _ = self.makeTestImages(seed=seed, nSrc=20, psfSize=refPsf, 

356 noiseLevel=refNoise, fluxLevel=fluxLevel) 

357 sciOriginal, src = self.makeTestImages(seed=seed, nSrc=20, psfSize=sciPsf, 

358 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

359 # Make a deep copy of the images first 

360 sciTest1 = sciOriginal.clone() 

361 refTest1 = refOriginal.clone() 

362 

363 # Basic AL, but we don't care about the result. 

364 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=False) 

365 self.assertMaskedImagesEqual(refOriginal.maskedImage, refTest1.maskedImage) 

366 

367 # Basic AL, but we don't care about the result. 

368 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=True) 

369 self.assertMaskedImagesEqual(refOriginal.maskedImage, refTest1.maskedImage) 

370 

371 def testSimAlSciNotModified(self): 

372 "Image differencing should not modify the original science image." 

373 refPsf = 2. 

374 sciPsfBase = 2. 

375 sciNoise = 5. 

376 refNoise = 1.5 

377 seed = 37 

378 fluxLevel = 500 

379 rng = np.random.RandomState(seed) 

380 alConfig = ImagePsfMatchConfig() 

381 

382 sciPsf = sciPsfBase + rng.random()*2. 

383 refOriginal, _ = self.makeTestImages(seed=seed, nSrc=20, psfSize=refPsf, 

384 noiseLevel=refNoise, fluxLevel=fluxLevel) 

385 sciOriginal, src = self.makeTestImages(seed=seed, nSrc=20, psfSize=sciPsf, 

386 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

387 # Make a deep copy of the images first 

388 sciTest1 = sciOriginal.clone() 

389 refTest1 = refOriginal.clone() 

390 

391 # Basic AL, but we don't care about the result. 

392 # Note that selecting KernelCandidates *does* change the science image slightly 

393 # because a background is subtracted before detection, then added back in. 

394 # For this test, we separate out that known modification by precomputing the 

395 # kernel candidates in wrapAlDiffim and using a deep copy of the science image. 

396 # This test is therefore checking that there are no other, unknown, modifications 

397 # of the science image. 

398 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=True, 

399 precomputeKernelCandidates=True) 

400 

401 self.assertMaskedImagesEqual(sciOriginal.maskedImage, sciTest1.maskedImage) 

402 

403 # Basic AL, but we don't care about the result. 

404 self.wrapAlDiffim(alConfig, refTest1, sciTest1, convolveTemplate=False, 

405 precomputeKernelCandidates=True) 

406 

407 self.assertMaskedImagesEqual(sciOriginal.maskedImage, sciTest1.maskedImage) 

408 

409 def testSimReverseAlNoDecorrEqualNoise(self): 

410 refPsf = 2. 

411 sciPsfBase = 2. 

412 sciNoise = 5. 

413 refNoise = 5 

414 seed = 37 

415 metricSigma = 0 

416 fluxLevel = 500 

417 rng = np.random.RandomState(seed) 

418 alConfig = ImagePsfMatchConfig() 

419 

420 for s in range(self.nRandIter): 

421 sciPsf = sciPsfBase + rng.random()*2. 

422 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf, 

423 noiseLevel=refNoise, fluxLevel=fluxLevel) 

424 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf, 

425 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

426 

427 res = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True) 

428 resR = self.wrapAlDiffim(alConfig, sci, ref, convolveTemplate=False) 

429 

430 metric = self.diffimMetricBasic(res, src, sigma=metricSigma) 

431 metricR = self.diffimMetricBasic(resR, src, sigma=metricSigma) 

432 # Alard&Lupton is not fully reversable, but the answers should be close. 

433 # Partly this needs the decorrelation afterburner 

434 # It might also be a difference in background subtraction 

435 self.assertFloatsAlmostEqual(metric, -metricR, atol=.1, rtol=.1) 

436 

437 def testSimReverseAlNoDecorrUnequalNoise(self): 

438 refPsf = 2. 

439 sciPsfBase = 2. 

440 sciNoise = 5. 

441 refNoise = 1.5 

442 seed = 37 

443 metricSigma = 0 

444 fluxLevel = 500 

445 rng = np.random.RandomState(seed) 

446 alConfig = ImagePsfMatchConfig() 

447 

448 for s in range(self.nRandIter): 

449 sciPsf = sciPsfBase + rng.random()*2. 

450 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf, 

451 noiseLevel=refNoise, fluxLevel=fluxLevel) 

452 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf, 

453 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

454 

455 res = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True) 

456 resR = self.wrapAlDiffim(alConfig, sci, ref, convolveTemplate=False) 

457 

458 metric = self.diffimMetricBasic(res, src, sigma=metricSigma) 

459 metricR = self.diffimMetricBasic(resR, src, sigma=metricSigma) 

460 # Alard&Lupton is not fully reversable, but the answers should be close. 

461 # Partly this needs the decorrelation afterburner 

462 # It might also be a difference in background subtraction 

463 self.assertFloatsAlmostEqual(metric, -metricR, atol=.1, rtol=.1) 

464 

465 

466class ImageDifferenceTestZogy(ImageDifferenceTestBase): 

467 

468 def testSimZogySciRefNotModified(self): 

469 "Image differencing should not modify the original images." 

470 refPsf = 2. 

471 sciPsfBase = 2. 

472 sciNoise = 5. 

473 refNoise = 1.5 

474 seed = 37 

475 fluxLevel = 500 

476 rng = np.random.RandomState(seed) 

477 zogyConfig = ZogyConfig() 

478 

479 sciPsf = sciPsfBase + rng.random()*2. 

480 refOriginal, _ = self.makeTestImages(seed=seed, nSrc=20, psfSize=refPsf, 

481 noiseLevel=refNoise, fluxLevel=fluxLevel) 

482 sciOriginal, src = self.makeTestImages(seed=seed, nSrc=20, psfSize=sciPsf, 

483 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

484 # Make a deep copy of the images first 

485 sciTest1 = sciOriginal.clone() 

486 refTest1 = refOriginal.clone() 

487 

488 # Basic ZOGY, but we don't care about the result. 

489 self.wrapZogyDiffim(zogyConfig, refTest1, sciTest1) 

490 self.assertMaskedImagesEqual(refOriginal.maskedImage, refTest1.maskedImage) 

491 self.assertMaskedImagesEqual(sciOriginal.maskedImage, sciTest1.maskedImage) 

492 

493 def testSimReverseZogy(self): 

494 refPsf = 2. 

495 sciPsfBase = 2. 

496 sciNoise = 5. 

497 refNoise = 1.5 

498 seed = 18 

499 fluxLevel = 500 

500 rng = np.random.RandomState(seed) 

501 zogyConfig = ZogyConfig() 

502 

503 for s in range(self.nRandIter): 

504 sciPsf = sciPsfBase + rng.random()*2. 

505 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf, 

506 noiseLevel=refNoise, fluxLevel=fluxLevel) 

507 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf, 

508 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

509 

510 res = self.wrapZogyDiffim(zogyConfig, ref, sci) 

511 resR = self.wrapZogyDiffim(zogyConfig, sci, ref) 

512 metric = self.diffimMetricBasic(res, src, sigma=3) 

513 metricR = self.diffimMetricBasic(resR, src, sigma=3) 

514 self.assertFloatsAlmostEqual(metric, -metricR) 

515 

516 

517class ImageDifferenceTestDecorrelation(ImageDifferenceTestBase): 

518 

519 def testSimAlDecorr(self): 

520 refPsf = 2. 

521 sciPsfBase = 2. 

522 sciNoise = 5. 

523 refNoise = 1.5 

524 seed = 37 

525 metricSigma = 0 

526 fluxLevel = 500 

527 rng = np.random.RandomState(seed) 

528 decorrelateConfig = DecorrelateALKernelConfig() 

529 decorrelate = DecorrelateALKernelTask(config=decorrelateConfig) 

530 alConfig = ImagePsfMatchConfig() 

531 

532 for s in range(self.nRandIter): 

533 sciPsf = sciPsfBase + rng.random()*2. 

534 ref, _ = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=refPsf, 

535 noiseLevel=refNoise, fluxLevel=fluxLevel) 

536 sci, src = self.makeTestImages(seed=seed + s, nSrc=20, psfSize=sciPsf, 

537 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

538 # The diffim tasks can modify the images, so make a deep copy to make sure they are independent 

539 sci2 = sci.clone() 

540 ref2 = ref.clone() 

541 

542 # Basic AL 

543 res = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True) 

544 

545 # Decorrelated AL 

546 mKernel = self.wrapAlDiffim(alConfig, ref, sci, convolveTemplate=True, returnKernel=True) 

547 resD = decorrelate.run(sci, ref, res, mKernel).correctedExposure 

548 metricD = self.diffimMetricBasic(resD, src, sigma=metricSigma) 

549 

550 # Swap the "science" and "reference" images, and alse swap which image is convolved. 

551 # The result is that the same image should be convolved as above 

552 resR = self.wrapAlDiffim(alConfig, sci2, ref2, convolveTemplate=False) 

553 

554 # Swap the images as above, and also decorrelate. 

555 mKernelR = self.wrapAlDiffim(alConfig, sci2, ref2, convolveTemplate=False, returnKernel=True) 

556 resDR = decorrelate.run(ref2, sci2, resR, mKernelR).correctedExposure 

557 metricDR = self.diffimMetricBasic(resDR, src, sigma=metricSigma) 

558 

559 self.assertFloatsAlmostEqual(metricD, -metricDR, atol=.1, rtol=0.1)