Coverage for tests/test_imageDifferenceOld.py: 14%

287 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-28 02:44 -0700

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 

23"""Tests of the old ImageDifferenceTask interface.""" 

24 

25import numpy as np 

26 

27import lsst.afw.geom as afwGeom 

28import lsst.afw.math as afwMath 

29import lsst.daf.base as dafBase 

30import lsst.geom as geom 

31from lsst.meas.algorithms.testUtils import plantSources 

32import lsst.utils.tests 

33 

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

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

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

37 

38 

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

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

41 

42 Attributes 

43 ---------- 

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

45 Bounding box of the test model. 

46 bufferSize : `int` 

47 Distance from the inner edge of the bounding box 

48 to avoid placing test sources in the model images. 

49 nRandIter : `int` 

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

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

52 Statistics control object. 

53 """ 

54 

55 def setUp(self): 

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

57 """ 

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

59 self.bufferSize = 5 

60 xSize = 250 

61 ySize = 260 

62 x0 = 12345 

63 y0 = 67890 

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

65 self.statsCtrl = afwMath.StatisticsControl() 

66 self.statsCtrl.setNumSigmaClip(3.) 

67 self.statsCtrl.setNumIter(3) 

68 

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

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

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

72 

73 Parameters 

74 ---------- 

75 seed : `int`, optional 

76 Seed value to initialize the random number generator. 

77 nSrc : `int`, optional 

78 Number of sources to simulate. 

79 psfSize : `float`, optional 

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

81 noiseLevel : `float`, optional 

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

83 fluxLevel : `float`, optional 

84 Reference flux of the simulated sources. 

85 fluxRange : `float`, optional 

86 Range in flux amplitude of the simulated sources. 

87 

88 Returns 

89 ------- 

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

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

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

93 Catalog of sources detected on the model image. 

94 """ 

95 rng = np.random.RandomState(seed) 

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

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

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

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

100 

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

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

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

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

105 skyLevel = 0 

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

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

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

109 model.image.array += noise 

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

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

112 

113 # Run source detection to set up the mask plane 

114 psfMatchTask = ImagePsfMatchTask(config=ImagePsfMatchConfig()) 

115 sourceCat = psfMatchTask.getSelectSources(model) 

116 

117 model.setWcs(self._makeWcs()) 

118 return model, sourceCat 

119 

120 @staticmethod 

121 def _makeWcs(offset=0): 

122 """Make a fake Wcs. 

123 

124 Parameters 

125 ---------- 

126 offset : `float` 

127 offset the Wcs by this many pixels. 

128 """ 

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

130 metadata = dafBase.PropertySet() 

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

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

133 metadata.set("NAXIS", 2) 

134 metadata.set("NAXIS1", 1024) 

135 metadata.set("NAXIS2", 1153) 

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

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

138 metadata.setDouble("CRVAL1", 215.604025685476) 

139 metadata.setDouble("CRVAL2", 53.1595451514076) 

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

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

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

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

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

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

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

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

148 return afwGeom.makeSkyWcs(metadata) 

149 

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

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

152 negative pixels in a residual image. 

153 

154 Parameters 

155 ---------- 

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

157 A residual image resulting from image differencing. 

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

159 Source catalog containing the locations to calculate the metric. 

160 radius : `int`, optional 

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

162 sigma : `float`, optional 

163 Threshold to include pixel values in the metric. 

164 

165 Returns 

166 ------- 

167 `float` 

168 Metric assessing the image differencing residual. 

169 """ 

170 nNeg = 0 

171 nPos = 0 

172 threshold = sigma*self.computeExposureStddev(residual) 

173 for src in sourceCat: 

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

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

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

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

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

179 

180 if (nPos + nNeg) == 0: 

181 metric = 0. 

182 else: 

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

184 return metric 

185 

186 def computeExposureStddev(self, exposure): 

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

188 

189 Parameters 

190 ---------- 

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

192 The input exposure. 

193 

194 Returns 

195 ------- 

196 `float` 

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

198 """ 

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

200 exposure.maskedImage.mask, 

201 afwMath.STDEVCLIP, self.statsCtrl) 

202 var = statObj.getValue(afwMath.STDEVCLIP) 

203 return var 

204 

205 @staticmethod 

206 def wrapZogyDiffim(config, templateExposure, scienceExposure): 

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

208 

209 Parameters 

210 ---------- 

211 config : `lsst.pex.config.Config` 

212 The image differencing Task configuration settings. 

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

214 The reference image to subtract from the science image. 

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

216 The science image. 

217 

218 Returns 

219 ------- 

220 `lsst.afw.image.ExposureF` 

221 The image difference. 

222 """ 

223 config.scaleByCalibration = False 

224 zogyTask = ZogyTask(config=config) 

225 

226 result = zogyTask.run(scienceExposure, templateExposure) 

227 return result.diffExp 

228 

229 @staticmethod 

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

231 precomputeKernelCandidates=False): 

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

233 

234 Parameters 

235 ---------- 

236 config : `lsst.pex.config.Config` 

237 The image differencing Task configuration settings. 

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

239 The reference image to subtract from the science image. 

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

241 The science image. 

242 convolveTemplate : `bool`, optional 

243 Option to convolve the template or the science image. 

244 returnKernel : `bool`, optional 

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

246 

247 Returns 

248 ------- 

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

250 The image difference, or the PSF matching kernel. 

251 """ 

252 alTask = ImagePsfMatchTask(config=config) 

253 candidateList = None 

254 if precomputeKernelCandidates: 

255 if convolveTemplate: 

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

257 else: 

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

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

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

261 result = alTask.subtractExposures(templateExposure, scienceExposure, 

262 templateFwhmPix=templateFwhmPix, 

263 scienceFwhmPix=scienceFwhmPix, 

264 doWarping=False, 

265 convolveTemplate=convolveTemplate, 

266 candidateList=candidateList, 

267 ) 

268 if returnKernel: 

269 return result.psfMatchingKernel 

270 else: 

271 return result.subtractedExposure 

272 

273 

274class ImageDifferenceTestVerification(ImageDifferenceTestBase): 

275 

276 def testModelImages(self): 

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

278 """ 

279 sciPsf = 2.4 

280 refPsf = 2. 

281 sciNoise = 5. 

282 refNoise = 1.5 

283 fluxRatio = refPsf**2/sciPsf**2 

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

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

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

287 

288 # Making the test images should be repeatable 

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

290 

291 diffIm = sciIm.clone() 

292 diffIm.image.array -= refIm.image.array 

293 

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

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

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

297 # but the peak should be much lower. 

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

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

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

301 

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

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

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

305 self.assertGreaterEqual(refMetric, -1) 

306 self.assertGreaterEqual(sciMetric, -1) 

307 

308 def testSimDiffim(self): 

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

310 refPsf = 2.4 

311 sciPsfBase = 2. 

312 sciNoise = 5. 

313 refNoise = 1.5 

314 seed = 8 

315 fluxLevel = 500 

316 decorrelate = DecorrelateALKernelTask() 

317 zogyConfig = ZogyConfig() 

318 alConfig = ImagePsfMatchConfig() 

319 

320 for s in range(self.nRandIter): 

321 sciPsf = sciPsfBase + s*0.2 

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

323 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

325 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

326 # The diffim tasks might modify the images, 

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

328 sci2 = sci.clone() 

329 ref2 = ref.clone() 

330 

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

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

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

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

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

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

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

338 self.assertGreaterEqual(metricZogy, -1) 

339 self.assertGreaterEqual(metricAl, -1) 

340 self.assertGreaterEqual(metricDecorr, -1) 

341 

342 

343class ImageDifferenceTestAlardLupton(ImageDifferenceTestBase): 

344 

345 def testSimAlRefNotModified(self): 

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

347 refPsf = 2. 

348 sciPsfBase = 2. 

349 sciNoise = 5. 

350 refNoise = 1.5 

351 seed = 37 

352 fluxLevel = 500 

353 rng = np.random.RandomState(seed) 

354 alConfig = ImagePsfMatchConfig() 

355 

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

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

358 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

360 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

361 # Make a deep copy of the images first 

362 sciTest1 = sciOriginal.clone() 

363 refTest1 = refOriginal.clone() 

364 

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

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

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

368 

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

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

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

372 

373 def testSimAlSciNotModified(self): 

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

375 refPsf = 2. 

376 sciPsfBase = 2. 

377 sciNoise = 5. 

378 refNoise = 1.5 

379 seed = 37 

380 fluxLevel = 500 

381 rng = np.random.RandomState(seed) 

382 alConfig = ImagePsfMatchConfig() 

383 

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

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

386 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

388 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

389 # Make a deep copy of the images first 

390 sciTest1 = sciOriginal.clone() 

391 refTest1 = refOriginal.clone() 

392 

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

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

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

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

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

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

399 # of the science image. 

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

401 precomputeKernelCandidates=True) 

402 

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

404 

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

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

407 precomputeKernelCandidates=True) 

408 

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

410 

411 def testSimReverseAlNoDecorrEqualNoise(self): 

412 refPsf = 2. 

413 sciPsfBase = 2. 

414 sciNoise = 5. 

415 refNoise = 5 

416 seed = 37 

417 metricSigma = 0 

418 fluxLevel = 500 

419 rng = np.random.RandomState(seed) 

420 alConfig = ImagePsfMatchConfig() 

421 

422 for s in range(self.nRandIter): 

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

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

425 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

427 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

428 

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

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

431 

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

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

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

435 # Partly this needs the decorrelation afterburner 

436 # It might also be a difference in background subtraction 

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

438 

439 def testSimReverseAlNoDecorrUnequalNoise(self): 

440 refPsf = 2. 

441 sciPsfBase = 2. 

442 sciNoise = 5. 

443 refNoise = 1.5 

444 seed = 37 

445 metricSigma = 0 

446 fluxLevel = 500 

447 rng = np.random.RandomState(seed) 

448 alConfig = ImagePsfMatchConfig() 

449 

450 for s in range(self.nRandIter): 

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

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

453 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

455 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

456 

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

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

459 

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

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

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

463 # Partly this needs the decorrelation afterburner 

464 # It might also be a difference in background subtraction 

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

466 

467 

468class ImageDifferenceTestZogy(ImageDifferenceTestBase): 

469 

470 def testSimZogySciRefNotModified(self): 

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

472 refPsf = 2. 

473 sciPsfBase = 2. 

474 sciNoise = 5. 

475 refNoise = 1.5 

476 seed = 37 

477 fluxLevel = 500 

478 rng = np.random.RandomState(seed) 

479 zogyConfig = ZogyConfig() 

480 

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

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

483 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

485 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

486 # Make a deep copy of the images first 

487 sciTest1 = sciOriginal.clone() 

488 refTest1 = refOriginal.clone() 

489 

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

491 self.wrapZogyDiffim(zogyConfig, refTest1, sciTest1) 

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

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

494 

495 def testSimReverseZogy(self): 

496 refPsf = 2. 

497 sciPsfBase = 2. 

498 sciNoise = 5. 

499 refNoise = 1.5 

500 seed = 18 

501 fluxLevel = 500 

502 rng = np.random.RandomState(seed) 

503 zogyConfig = ZogyConfig() 

504 

505 for s in range(self.nRandIter): 

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

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

508 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

510 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

511 

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

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

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

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

516 self.assertFloatsAlmostEqual(metric, -metricR) 

517 

518 

519class ImageDifferenceTestDecorrelation(ImageDifferenceTestBase): 

520 

521 def testSimAlDecorr(self): 

522 refPsf = 2. 

523 sciPsfBase = 2. 

524 sciNoise = 5. 

525 refNoise = 1.5 

526 seed = 37 

527 metricSigma = 0 

528 fluxLevel = 500 

529 rng = np.random.RandomState(seed) 

530 decorrelateConfig = DecorrelateALKernelConfig() 

531 decorrelate = DecorrelateALKernelTask(config=decorrelateConfig) 

532 alConfig = ImagePsfMatchConfig() 

533 

534 for s in range(self.nRandIter): 

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

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

537 noiseLevel=refNoise, fluxLevel=fluxLevel) 

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

539 noiseLevel=sciNoise, fluxLevel=fluxLevel) 

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

541 sci2 = sci.clone() 

542 ref2 = ref.clone() 

543 

544 # Basic AL 

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

546 

547 # Decorrelated AL 

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

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

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

551 

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

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

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

555 

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

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

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

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

560 

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