Coverage for tests/test_kernel.py: 5%

604 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-15 02:45 -0700

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010 LSST Corporation. 

4# 

5# This product includes software developed by the 

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

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 <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22import math 

23import re 

24import unittest 

25 

26import numpy as np 

27from numpy.testing import assert_allclose 

28 

29import lsst.utils.tests 

30import lsst.pex.exceptions as pexExcept 

31import lsst.geom 

32import lsst.afw.image as afwImage 

33import lsst.afw.math as afwMath 

34 

35 

36def makeGaussianKernelList(kWidth, kHeight, gaussParamsList): 

37 """Create a list of gaussian kernels. 

38 

39 This is useful for constructing a LinearCombinationKernel. 

40 

41 Inputs: 

42 - kWidth, kHeight: width and height of kernel 

43 - gaussParamsList: a list of parameters for GaussianFunction2D (each a 3-tuple of floats) 

44 """ 

45 kVec = [] 

46 for majorSigma, minorSigma, angle in gaussParamsList: 

47 kFunc = afwMath.GaussianFunction2D(majorSigma, minorSigma, angle) 

48 kVec.append(afwMath.AnalyticKernel(kWidth, kHeight, kFunc)) 

49 return kVec 

50 

51 

52def makeDeltaFunctionKernelList(kWidth, kHeight): 

53 """Create a list of delta function kernels 

54 

55 This is useful for constructing a LinearCombinationKernel. 

56 """ 

57 kVec = [] 

58 for activeCol in range(kWidth): 

59 for activeRow in range(kHeight): 

60 kVec.append(afwMath.DeltaFunctionKernel( 

61 kWidth, kHeight, lsst.geom.Point2I(activeCol, activeRow))) 

62 return kVec 

63 

64 

65class KernelTestCase(lsst.utils.tests.TestCase): 

66 """A test case for Kernels""" 

67 

68 def testAnalyticKernel(self): 

69 """Test AnalyticKernel using a Gaussian function 

70 """ 

71 kWidth = 5 

72 kHeight = 8 

73 

74 gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) 

75 kernel = afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc) 

76 center = kernel.getCtr() 

77 self.basicTests(kernel, 3, dimMustMatch=False) 

78 

79 kernelResized = self.verifyResized(kernel) 

80 self.basicTests(kernelResized, 3, dimMustMatch=False) 

81 

82 fArr = np.zeros( 

83 shape=[kernel.getWidth(), kernel.getHeight()], dtype=float) 

84 for xsigma in (0.1, 1.0, 3.0): 

85 for ysigma in (0.1, 1.0, 3.0): 

86 gaussFunc.setParameters((xsigma, ysigma, 0.0)) 

87 # compute array of function values and normalize 

88 for row in range(kernel.getHeight()): 

89 y = row - center.getY() 

90 for col in range(kernel.getWidth()): 

91 x = col - center.getX() 

92 fArr[col, row] = gaussFunc(x, y) 

93 fArr /= fArr.sum() 

94 

95 kernel.setKernelParameters((xsigma, ysigma, 0.0)) 

96 kImage = afwImage.ImageD(kernel.getDimensions()) 

97 kernel.computeImage(kImage, True) 

98 

99 kArr = kImage.getArray().transpose() 

100 if not np.allclose(fArr, kArr): 

101 self.fail(f"{kernel.__class__.__name__} = {kArr} " 

102 f"for xsigma={xsigma}, ysigma={ysigma}") 

103 

104 kernel.setKernelParameters((0.5, 1.1, 0.3)) 

105 kernelClone = kernel.clone() 

106 errStr = self.compareKernels(kernel, kernelClone) 

107 if errStr: 

108 self.fail(errStr) 

109 

110 kernel.setKernelParameters((1.5, 0.2, 0.7)) 

111 errStr = self.compareKernels(kernel, kernelClone) 

112 if not errStr: 

113 self.fail( 

114 "Clone was modified by changing original's kernel parameters") 

115 

116 self.verifyCache(kernel, hasCache=False) 

117 

118 def verifyResized(self, kernel, testClip=True, oddPadRaises=False): 

119 """Verify kernels can be resized properly. 

120 

121 Parameters 

122 ---------- 

123 testClip : bool 

124 Test whether a kernel can be safely resized such that one 

125 dimension is shorter than original. 

126 oddPadRaises : bool 

127 Test that attempting to resize by an odd number of pixels raises 

128 an InvalidParameterError. 

129 Fixed/Delta Kernels cannot be resized by an odd amount because 

130 they are padded/clipped rather than reevalutated like Analytic Kernels. 

131 

132 Returns 

133 ------- 

134 kernelResized: `Kernel` 

135 The last resized Kernel that was instantiated and tested 

136 """ 

137 

138 kWidth, kHeight = kernel.getDimensions() 

139 

140 badDims = [(0, 0), (0, 1), (1, 0), (-1, 1), (3, -3)] # impossible kernel dimensions 

141 goodPads = [(0, 0), (4, 2), (2, 4)] # all kernels can resize by even, non-negative number of pixels 

142 oddPads = [(3, 3), (1, 3), (3, 2), (0, 3)] # at least one padding dimension is odd 

143 clippingPads = [(-2, -2), (2, -2), (-2, 2)] # at least one dimension is smaller than orig 

144 

145 if oddPadRaises: 

146 badDims.extend([(kWidth + w, kHeight + h) for (w, h) in oddPads]) 

147 else: 

148 goodPads.extend(oddPads) 

149 

150 if testClip: 

151 goodPads.extend(clippingPads) 

152 

153 # Check that error is raised when bad dimensions specified (and optionally odd padding amounts) 

154 for badWidth, badHeight in badDims: 

155 with self.assertRaises(pexExcept.InvalidParameterError): 

156 kernelResized = kernel.resized(badWidth, badHeight) 

157 

158 # Check that resizing is successful 

159 for padX, padY in goodPads: 

160 # Do not test if padding will make resized kernel smaller than 0 

161 if kWidth <= -padX or kHeight <= -padY: 

162 with self.assertRaises(pexExcept.InvalidParameterError): 

163 kernelResized = kernel.resized(kWidth + padX, kHeight + padY) 

164 kernelResized = kernel.resized(kWidth + padX, kHeight + padY) 

165 self.assertEqual(kernel.getWidth() + padX, kernelResized.getWidth()) 

166 self.assertEqual(kernel.getHeight() + padY, kernelResized.getHeight()) 

167 errStr = self.compareResizedKernels(kernel, kernelResized) 

168 if errStr: 

169 self.fail(errStr) 

170 

171 return kernelResized 

172 

173 def verifyCache(self, kernel, hasCache=False): 

174 """Verify the kernel cache 

175 

176 @param kernel: kernel to test 

177 @param hasCache: set True if this kind of kernel supports a cache, False otherwise 

178 """ 

179 for cacheSize in (0, 100, 2000): 

180 kernel.computeCache(cacheSize) 

181 self.assertEqual(kernel.getCacheSize(), 

182 cacheSize if hasCache else 0) 

183 kernelCopy = kernel.clone() 

184 self.assertEqual(kernelCopy.getCacheSize(), kernel.getCacheSize()) 

185 

186 def testShrinkGrowBBox(self): 

187 """Test Kernel methods shrinkBBox and growBBox 

188 """ 

189 boxStart = lsst.geom.Point2I(3, -3) 

190 for kWidth in (1, 2, 6): 

191 for kHeight in (1, 2, 5): 

192 for deltaWidth in (-1, 0, 1, 20): 

193 fullWidth = kWidth + deltaWidth 

194 for deltaHeight in (-1, 0, 1, 20): 

195 fullHeight = kHeight + deltaHeight 

196 kernel = afwMath.DeltaFunctionKernel( 

197 kWidth, kHeight, lsst.geom.Point2I(0, 0)) 

198 fullBBox = lsst.geom.Box2I( 

199 boxStart, lsst.geom.Extent2I(fullWidth, fullHeight)) 

200 if (fullWidth < kWidth) or (fullHeight < kHeight): 

201 self.assertRaises( 

202 Exception, kernel.shrinkBBox, fullBBox) 

203 continue 

204 

205 shrunkBBox = kernel.shrinkBBox(fullBBox) 

206 self.assertEqual(shrunkBBox.getWidth(), 

207 fullWidth + 1 - kWidth) 

208 self.assertEqual(shrunkBBox.getHeight(), 

209 fullHeight + 1 - kHeight) 

210 self.assertEqual(shrunkBBox.getMin(), boxStart + lsst.geom.Extent2I(kernel.getCtr())) 

211 newFullBBox = kernel.growBBox(shrunkBBox) 

212 self.assertEqual(newFullBBox, fullBBox, 

213 "growBBox(shrinkBBox(x)) != x") 

214 

215 def testDeltaFunctionKernel(self): 

216 """Test DeltaFunctionKernel 

217 """ 

218 for kWidth in range(1, 4): 

219 for kHeight in range(1, 4): 

220 for activeCol in range(kWidth): 

221 for activeRow in range(kHeight): 

222 kernel = afwMath.DeltaFunctionKernel(kWidth, kHeight, 

223 lsst.geom.Point2I(activeCol, activeRow)) 

224 kImage = afwImage.ImageD(kernel.getDimensions()) 

225 kSum = kernel.computeImage(kImage, False) 

226 self.assertEqual(kSum, 1.0) 

227 kArr = kImage.getArray().transpose() 

228 self.assertEqual(kArr[activeCol, activeRow], 1.0) 

229 kArr[activeCol, activeRow] = 0.0 

230 self.assertEqual(kArr.sum(), 0.0) 

231 

232 errStr = self.compareKernels(kernel, kernel.clone()) 

233 if errStr: 

234 self.fail(errStr) 

235 

236 self.assertRaises(pexExcept.InvalidParameterError, 

237 afwMath.DeltaFunctionKernel, 0, kHeight, lsst.geom.Point2I(kWidth, kHeight)) 

238 self.assertRaises(pexExcept.InvalidParameterError, 

239 afwMath.DeltaFunctionKernel, kWidth, 0, lsst.geom.Point2I(kWidth, kHeight)) 

240 

241 kernel = afwMath.DeltaFunctionKernel(5, 6, lsst.geom.Point2I(1, 1)) 

242 self.basicTests(kernel, 0) 

243 

244 kernelResized = self.verifyResized(kernel, oddPadRaises=True) 

245 self.basicTests(kernelResized, 0) 

246 self.verifyCache(kernel, hasCache=False) 

247 

248 def testFixedKernel(self): 

249 """Test FixedKernel using a ramp function 

250 """ 

251 kWidth = 5 

252 kHeight = 6 

253 

254 inArr = np.arange(kWidth * kHeight, dtype=float) 

255 inArr.shape = [kWidth, kHeight] 

256 

257 inImage = afwImage.ImageD(lsst.geom.Extent2I(kWidth, kHeight)) 

258 for row in range(inImage.getHeight()): 

259 for col in range(inImage.getWidth()): 

260 inImage[col, row, afwImage.LOCAL] = inArr[col, row] 

261 

262 kernel = afwMath.FixedKernel(inImage) 

263 self.basicTests(kernel, 0) 

264 

265 kernelResized = self.verifyResized(kernel, oddPadRaises=True) 

266 self.basicTests(kernelResized, 0) 

267 

268 outImage = afwImage.ImageD(kernel.getDimensions()) 

269 kernel.computeImage(outImage, False) 

270 

271 outArr = outImage.getArray().transpose() 

272 if not np.allclose(inArr, outArr): 

273 self.fail(f"{kernel.__class__.__name__} = {inArr} != {outArr} (not normalized)") 

274 

275 normInArr = inArr / inArr.sum() 

276 normOutImage = afwImage.ImageD(kernel.getDimensions()) 

277 kernel.computeImage(normOutImage, True) 

278 normOutArr = normOutImage.getArray().transpose() 

279 if not np.allclose(normOutArr, normInArr): 

280 self.fail(f"{kernel.__class__.__name__} = {normInArr} != {normOutArr} (normalized)") 

281 

282 errStr = self.compareKernels(kernel, kernel.clone()) 

283 if errStr: 

284 self.fail(errStr) 

285 

286 self.verifyCache(kernel, hasCache=False) 

287 

288 def testLinearCombinationKernelDelta(self): 

289 """Test LinearCombinationKernel using a set of delta basis functions 

290 """ 

291 kWidth = 3 

292 kHeight = 2 

293 

294 # create list of kernels 

295 basisKernelList = makeDeltaFunctionKernelList(kWidth, kHeight) 

296 basisImArrList = [] 

297 for basisKernel in basisKernelList: 

298 basisImage = afwImage.ImageD(basisKernel.getDimensions()) 

299 basisKernel.computeImage(basisImage, True) 

300 basisImArrList.append(basisImage.getArray()) 

301 

302 kParams = [0.0]*len(basisKernelList) 

303 kernel = afwMath.LinearCombinationKernel(basisKernelList, kParams) 

304 self.assertTrue(kernel.isDeltaFunctionBasis()) 

305 self.basicTests(kernel, len(kParams)) 

306 kernelResized = self.verifyResized(kernel, testClip=False, oddPadRaises=True) 

307 self.basicTests(kernelResized, len(kParams)) 

308 for ii in range(len(basisKernelList)): 

309 kParams = [0.0]*len(basisKernelList) 

310 kParams[ii] = 1.0 

311 kernel.setKernelParameters(kParams) 

312 kIm = afwImage.ImageD(kernel.getDimensions()) 

313 kernel.computeImage(kIm, True) 

314 kImArr = kIm.getArray() 

315 if not np.allclose(kImArr, basisImArrList[ii]): 

316 self.fail(f"{kernel.__class__.__name__} = {kImArr} != " 

317 f"{basisImArrList[ii]} for the {ii}'th basis kernel") 

318 

319 kernelClone = kernel.clone() 

320 errStr = self.compareKernels(kernel, kernelClone) 

321 if errStr: 

322 self.fail(errStr) 

323 

324 self.verifyCache(kernel, hasCache=False) 

325 

326 def testComputeImageRaise(self): 

327 """Test Kernel.computeImage raises OverflowException iff doNormalize True and kernel sum exactly 0 

328 """ 

329 kWidth = 4 

330 kHeight = 3 

331 

332 polyFunc1 = afwMath.PolynomialFunction1D(0) 

333 polyFunc2 = afwMath.PolynomialFunction2D(0) 

334 analKernel = afwMath.AnalyticKernel(kWidth, kHeight, polyFunc2) 

335 kImage = afwImage.ImageD(analKernel.getDimensions()) 

336 for coeff in (-1, -1e-99, 0, 1e99, 1): 

337 analKernel.setKernelParameters([coeff]) 

338 

339 analKernel.computeImage(kImage, False) 

340 fixedKernel = afwMath.FixedKernel(kImage) 

341 

342 sepKernel = afwMath.SeparableKernel( 

343 kWidth, kHeight, polyFunc1, polyFunc1) 

344 sepKernel.setKernelParameters([coeff, coeff]) 

345 

346 kernelList = [] 

347 kernelList.append(analKernel) 

348 lcKernel = afwMath.LinearCombinationKernel(kernelList, [1]) 

349 self.assertFalse(lcKernel.isDeltaFunctionBasis()) 

350 

351 doRaise = (coeff == 0) 

352 self.basicTestComputeImageRaise( 

353 analKernel, doRaise, "AnalyticKernel") 

354 self.basicTestComputeImageRaise( 

355 fixedKernel, doRaise, "FixedKernel") 

356 self.basicTestComputeImageRaise( 

357 sepKernel, doRaise, "SeparableKernel") 

358 self.basicTestComputeImageRaise( 

359 lcKernel, doRaise, "LinearCombinationKernel") 

360 

361 lcKernel.setKernelParameters([0]) 

362 self.basicTestComputeImageRaise( 

363 lcKernel, True, "LinearCombinationKernel") 

364 

365 def testLinearCombinationKernelAnalytic(self): 

366 """Test LinearCombinationKernel using analytic basis kernels. 

367 

368 The basis kernels are mutable so that we can verify that the 

369 LinearCombinationKernel has private copies of the basis kernels. 

370 """ 

371 kWidth = 5 

372 kHeight = 8 

373 

374 # create list of kernels 

375 basisImArrList = [] 

376 basisKernelList = [] 

377 for basisKernelParams in [(1.2, 0.3, 1.570796), (1.0, 0.2, 0.0)]: 

378 basisKernelFunction = afwMath.GaussianFunction2D( 

379 *basisKernelParams) 

380 basisKernel = afwMath.AnalyticKernel( 

381 kWidth, kHeight, basisKernelFunction) 

382 basisImage = afwImage.ImageD(basisKernel.getDimensions()) 

383 basisKernel.computeImage(basisImage, True) 

384 basisImArrList.append(basisImage.getArray()) 

385 basisKernelList.append(basisKernel) 

386 

387 kParams = [0.0]*len(basisKernelList) 

388 kernel = afwMath.LinearCombinationKernel(basisKernelList, kParams) 

389 self.assertTrue(not kernel.isDeltaFunctionBasis()) 

390 self.basicTests(kernel, len(kParams)) 

391 

392 kernelResized = self.verifyResized(kernel) 

393 self.basicTests(kernelResized, len(kParams)) 

394 

395 # make sure the linear combination kernel has private copies of its basis kernels 

396 # by altering the local basis kernels and making sure the new images do 

397 # NOT match 

398 modBasisImArrList = [] 

399 for basisKernel in basisKernelList: 

400 basisKernel.setKernelParameters((0.4, 0.5, 0.6)) 

401 modBasisImage = afwImage.ImageD(basisKernel.getDimensions()) 

402 basisKernel.computeImage(modBasisImage, True) 

403 modBasisImArrList.append(modBasisImage.getArray()) 

404 

405 for ii in range(len(basisKernelList)): 

406 kParams = [0.0]*len(basisKernelList) 

407 kParams[ii] = 1.0 

408 kernel.setKernelParameters(kParams) 

409 kIm = afwImage.ImageD(kernel.getDimensions()) 

410 kernel.computeImage(kIm, True) 

411 kImArr = kIm.getArray() 

412 if not np.allclose(kImArr, basisImArrList[ii]): 

413 self.fail(f"{kernel.__class__.__name__} = {kImArr} != " 

414 f"{basisImArrList[ii]} for the {ii}'th basis kernel") 

415 if np.allclose(kImArr, modBasisImArrList[ii]): 

416 self.fail(f"{kernel.__class__.__name__} = {kImArr} != " 

417 f"{modBasisImArrList[ii]} for *modified* {ii}'th basis kernel") 

418 

419 kernelClone = kernel.clone() 

420 errStr = self.compareKernels(kernel, kernelClone) 

421 if errStr: 

422 self.fail(errStr) 

423 

424 self.verifyCache(kernel, hasCache=False) 

425 

426 def testSeparableKernel(self): 

427 """Test SeparableKernel using a Gaussian function 

428 """ 

429 kWidth = 5 

430 kHeight = 8 

431 

432 gaussFunc1 = afwMath.GaussianFunction1D(1.0) 

433 kernel = afwMath.SeparableKernel( 

434 kWidth, kHeight, gaussFunc1, gaussFunc1) 

435 center = kernel.getCtr() 

436 self.basicTests(kernel, 2) 

437 

438 kernelResized = self.verifyResized(kernel) 

439 self.basicTests(kernelResized, 2) 

440 

441 fArr = np.zeros( 

442 shape=[kernel.getWidth(), kernel.getHeight()], dtype=float) 

443 gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) 

444 for xsigma in (0.1, 1.0, 3.0): 

445 gaussFunc1.setParameters((xsigma,)) 

446 for ysigma in (0.1, 1.0, 3.0): 

447 gaussFunc.setParameters((xsigma, ysigma, 0.0)) 

448 # compute array of function values and normalize 

449 for row in range(kernel.getHeight()): 

450 y = row - center.getY() 

451 for col in range(kernel.getWidth()): 

452 x = col - center.getX() 

453 fArr[col, row] = gaussFunc(x, y) 

454 fArr /= fArr.sum() 

455 

456 kernel.setKernelParameters((xsigma, ysigma)) 

457 kImage = afwImage.ImageD(kernel.getDimensions()) 

458 kernel.computeImage(kImage, True) 

459 kArr = kImage.getArray().transpose() 

460 if not np.allclose(fArr, kArr): 

461 self.fail(f"{kernel.__class__.__name__} = {kArr} != " 

462 f"{fArr} for xsigma={xsigma}, ysigma={ysigma}") 

463 kernelClone = kernel.clone() 

464 errStr = self.compareKernels(kernel, kernelClone) 

465 if errStr: 

466 self.fail(errStr) 

467 

468 kernel.setKernelParameters((1.2, 0.6)) 

469 errStr = self.compareKernels(kernel, kernelClone) 

470 if not errStr: 

471 self.fail( 

472 "Clone was modified by changing original's kernel parameters") 

473 

474 self.verifyCache(kernel, hasCache=True) 

475 

476 def testMakeBadKernels(self): 

477 """Attempt to make various invalid kernels; make sure the constructor shows an exception 

478 """ 

479 kWidth = 4 

480 kHeight = 3 

481 

482 gaussFunc1 = afwMath.GaussianFunction1D(1.0) 

483 gaussFunc2 = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) 

484 spFunc = afwMath.PolynomialFunction2D(1) 

485 kernelList = [] 

486 kernelList.append(afwMath.FixedKernel( 

487 afwImage.ImageD(lsst.geom.Extent2I(kWidth, kHeight), 0.1))) 

488 kernelList.append(afwMath.FixedKernel( 

489 afwImage.ImageD(lsst.geom.Extent2I(kWidth, kHeight), 0.2))) 

490 

491 for numKernelParams in (2, 4): 

492 spFuncList = [] 

493 for ii in range(numKernelParams): 

494 spFuncList.append(spFunc.clone()) 

495 try: 

496 afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc2, spFuncList) 

497 self.fail("Should have failed with wrong # of spatial functions") 

498 except pexExcept.Exception: 

499 pass 

500 

501 for numKernelParams in (1, 3): 

502 spFuncList = [] 

503 for ii in range(numKernelParams): 

504 spFuncList.append(spFunc.clone()) 

505 try: 

506 afwMath.LinearCombinationKernel(kernelList, spFuncList) 

507 self.fail("Should have failed with wrong # of spatial functions") 

508 except pexExcept.Exception: 

509 pass 

510 kParamList = [0.2]*numKernelParams 

511 try: 

512 afwMath.LinearCombinationKernel(kernelList, kParamList) 

513 self.fail("Should have failed with wrong # of kernel parameters") 

514 except pexExcept.Exception: 

515 pass 

516 try: 

517 afwMath.SeparableKernel( 

518 kWidth, kHeight, gaussFunc1, gaussFunc1, spFuncList) 

519 self.fail("Should have failed with wrong # of spatial functions") 

520 except pexExcept.Exception: 

521 pass 

522 

523 for pointX in range(-1, kWidth+2): 

524 for pointY in range(-1, kHeight+2): 

525 if (0 <= pointX < kWidth) and (0 <= pointY < kHeight): 

526 continue 

527 try: 

528 afwMath.DeltaFunctionKernel( 

529 kWidth, kHeight, lsst.geom.Point2I(pointX, pointY)) 

530 self.fail("Should have failed with point not on kernel") 

531 except pexExcept.Exception: 

532 pass 

533 

534 def testSVAnalyticKernel(self): 

535 """Test spatially varying AnalyticKernel using a Gaussian function 

536 

537 Just tests cloning. 

538 """ 

539 kWidth = 5 

540 kHeight = 8 

541 

542 # spatial model 

543 spFunc = afwMath.PolynomialFunction2D(1) 

544 

545 # spatial parameters are a list of entries, one per kernel parameter; 

546 # each entry is a list of spatial parameters 

547 sParams = ( 

548 (1.0, 1.0, 0.0), 

549 (1.0, 0.0, 1.0), 

550 (0.5, 0.5, 0.5), 

551 ) 

552 

553 gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) 

554 kernel = afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc, spFunc) 

555 kernel.setSpatialParameters(sParams) 

556 

557 kernelClone = kernel.clone() 

558 errStr = self.compareKernels(kernel, kernelClone) 

559 if errStr: 

560 self.fail(errStr) 

561 

562 newSParams = ( 

563 (0.1, 0.2, 0.5), 

564 (0.1, 0.5, 0.2), 

565 (0.2, 0.3, 0.3), 

566 ) 

567 kernel.setSpatialParameters(newSParams) 

568 errStr = self.compareKernels(kernel, kernelClone) 

569 if not errStr: 

570 self.fail( 

571 "Clone was modified by changing original's spatial parameters") 

572 

573 # 

574 # check that we can construct a FixedKernel from a LinearCombinationKernel 

575 # 

576 x, y = 100, 200 

577 kernel2 = afwMath.FixedKernel(kernel, lsst.geom.PointD(x, y)) 

578 

579 self.assertTrue(re.search("AnalyticKernel", kernel.toString())) 

580 self.assertFalse(kernel2.isSpatiallyVarying()) 

581 

582 self.assertTrue(re.search("FixedKernel", kernel2.toString())) 

583 self.assertTrue(kernel.isSpatiallyVarying()) 

584 

585 kim = afwImage.ImageD(kernel.getDimensions()) 

586 kernel.computeImage(kim, True, x, y) 

587 

588 kim2 = afwImage.ImageD(kernel2.getDimensions()) 

589 kernel2.computeImage(kim2, True) 

590 

591 assert_allclose(kim.getArray(), kim2.getArray()) 

592 

593 def testSVLinearCombinationKernelFixed(self): 

594 """Test a spatially varying LinearCombinationKernel whose bases are FixedKernels""" 

595 kWidth = 3 

596 kHeight = 2 

597 

598 # create image arrays for the basis kernels 

599 basisImArrList = [] 

600 imArr = np.zeros((kWidth, kHeight), dtype=float) 

601 imArr += 0.1 

602 imArr[kWidth//2, :] = 0.9 

603 basisImArrList.append(imArr) 

604 imArr = np.zeros((kWidth, kHeight), dtype=float) 

605 imArr += 0.2 

606 imArr[:, kHeight//2] = 0.8 

607 basisImArrList.append(imArr) 

608 

609 # create a list of basis kernels from the images 

610 basisKernelList = [] 

611 for basisImArr in basisImArrList: 

612 basisImage = afwImage.makeImageFromArray( 

613 basisImArr.transpose().copy()) 

614 kernel = afwMath.FixedKernel(basisImage) 

615 basisKernelList.append(kernel) 

616 

617 # create spatially varying linear combination kernel 

618 spFunc = afwMath.PolynomialFunction2D(1) 

619 

620 # spatial parameters are a list of entries, one per kernel parameter; 

621 # each entry is a list of spatial parameters 

622 sParams = ( 

623 (0.0, 1.0, 0.0), 

624 (0.0, 0.0, 1.0), 

625 ) 

626 

627 kernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

628 self.assertFalse(kernel.isDeltaFunctionBasis()) 

629 self.basicTests(kernel, 2, nSpatialParams=3) 

630 kernel.setSpatialParameters(sParams) 

631 kImage = afwImage.ImageD(lsst.geom.Extent2I(kWidth, kHeight)) 

632 for colPos, rowPos, coeff0, coeff1 in [ 

633 (0.0, 0.0, 0.0, 0.0), 

634 (1.0, 0.0, 1.0, 0.0), 

635 (0.0, 1.0, 0.0, 1.0), 

636 (1.0, 1.0, 1.0, 1.0), 

637 (0.5, 0.5, 0.5, 0.5), 

638 ]: 

639 kernel.computeImage(kImage, False, colPos, rowPos) 

640 kImArr = kImage.getArray().transpose() 

641 refKImArr = (basisImArrList[0] * coeff0) + \ 

642 (basisImArrList[1] * coeff1) 

643 if not np.allclose(kImArr, refKImArr): 

644 self.fail(f"{kernel.__class__.__name__} = {kImArr} != " 

645 f"{refKImArr} at colPos={colPos}, rowPos={rowPos}") 

646 

647 sParams = ( 

648 (0.1, 1.0, 0.0), 

649 (0.1, 0.0, 1.0), 

650 ) 

651 kernel.setSpatialParameters(sParams) 

652 kernelClone = kernel.clone() 

653 errStr = self.compareKernels(kernel, kernelClone) 

654 if errStr: 

655 self.fail(errStr) 

656 

657 newSParams = ( 

658 (0.1, 0.2, 0.5), 

659 (0.1, 0.5, 0.2), 

660 ) 

661 kernel.setSpatialParameters(newSParams) 

662 errStr = self.compareKernels(kernel, kernelClone) 

663 if not errStr: 

664 self.fail( 

665 "Clone was modified by changing original's spatial parameters") 

666 

667 def testSVSeparableKernel(self): 

668 """Test spatially varying SeparableKernel using a Gaussian function 

669 

670 Just tests cloning. 

671 """ 

672 kWidth = 5 

673 kHeight = 8 

674 

675 # spatial model 

676 spFunc = afwMath.PolynomialFunction2D(1) 

677 

678 # spatial parameters are a list of entries, one per kernel parameter; 

679 # each entry is a list of spatial parameters 

680 sParams = ( 

681 (1.0, 1.0, 0.0), 

682 (1.0, 0.0, 1.0), 

683 ) 

684 

685 gaussFunc = afwMath.GaussianFunction1D(1.0) 

686 kernel = afwMath.SeparableKernel( 

687 kWidth, kHeight, gaussFunc, gaussFunc, spFunc) 

688 kernel.setSpatialParameters(sParams) 

689 

690 kernelClone = kernel.clone() 

691 errStr = self.compareKernels(kernel, kernelClone) 

692 if errStr: 

693 self.fail(errStr) 

694 

695 newSParams = ( 

696 (0.1, 0.2, 0.5), 

697 (0.1, 0.5, 0.2), 

698 ) 

699 kernel.setSpatialParameters(newSParams) 

700 errStr = self.compareKernels(kernel, kernelClone) 

701 if not errStr: 

702 self.fail( 

703 "Clone was modified by changing original's spatial parameters") 

704 

705 def testSetCtr(self): 

706 """Test setCtrCol/Row""" 

707 kWidth = 3 

708 kHeight = 4 

709 

710 gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) 

711 kernel = afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc) 

712 for xCtr in range(kWidth): 

713 for yCtr in range(kHeight): 

714 center = lsst.geom.Point2I(xCtr, yCtr) 

715 kernel.setCtr(center) 

716 self.assertEqual(kernel.getCtr(), center) 

717 

718 def testZeroSizeKernel(self): 

719 """Creating a kernel with width or height < 1 should raise an exception. 

720 

721 Note: this ignores the default constructors, which produce kernels with height = width = 0. 

722 The default constructors are only intended to support persistence, not to produce useful kernels. 

723 """ 

724 gaussFunc2D = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) 

725 gaussFunc1D = afwMath.GaussianFunction1D(1.0) 

726 zeroPoint = lsst.geom.Point2I(0, 0) 

727 for kWidth in (-1, 0, 1): 

728 for kHeight in (-1, 0, 1): 

729 if (kHeight > 0) and (kWidth > 0): 

730 continue 

731 if (kHeight >= 0) and (kWidth >= 0): 

732 # don't try to create an image with negative dimensions 

733 blankImage = afwImage.ImageF( 

734 lsst.geom.Extent2I(kWidth, kHeight)) 

735 self.assertRaises( 

736 Exception, afwMath.FixedKernel, blankImage) 

737 self.assertRaises( 

738 Exception, afwMath.AnalyticKernel, kWidth, kHeight, gaussFunc2D) 

739 self.assertRaises(Exception, afwMath.SeparableKernel, 

740 kWidth, kHeight, gaussFunc1D, gaussFunc1D) 

741 self.assertRaises( 

742 Exception, afwMath.DeltaFunctionKernel, kWidth, kHeight, zeroPoint) 

743 

744 def testRefactorDeltaLinearCombinationKernel(self): 

745 """Test LinearCombinationKernel.refactor with delta function basis kernels 

746 """ 

747 kWidth = 4 

748 kHeight = 3 

749 

750 for spOrder in (0, 1, 2): 

751 spFunc = afwMath.PolynomialFunction2D(spOrder) 

752 numSpParams = spFunc.getNParameters() 

753 

754 basisKernelList = makeDeltaFunctionKernelList(kWidth, kHeight) 

755 kernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

756 

757 numBasisKernels = kernel.getNKernelParameters() 

758 maxVal = 1.01 + ((numSpParams - 1) * 0.1) 

759 sParamList = [np.arange(kInd + 1.0, kInd + maxVal, 0.1) 

760 for kInd in range(numBasisKernels)] 

761 kernel.setSpatialParameters(sParamList) 

762 

763 refKernel = kernel.refactor() 

764 self.assertTrue(refKernel) 

765 errStr = self.compareKernels( 

766 kernel, refKernel, compareParams=False) 

767 if errStr: 

768 self.fail(f"failed with {errStr} for spOrder={spOrder}, numSpParams={numSpParams}") 

769 

770 def testRefactorGaussianLinearCombinationKernel(self): 

771 """Test LinearCombinationKernel.refactor with Gaussian basis kernels 

772 """ 

773 kWidth = 4 

774 kHeight = 3 

775 

776 for spOrder in (0, 1, 2): 

777 spFunc = afwMath.PolynomialFunction2D(spOrder) 

778 numSpParams = spFunc.getNParameters() 

779 

780 gaussParamsList = [ 

781 (1.5, 1.5, 0.0), 

782 (2.5, 1.5, 0.0), 

783 (2.5, 1.5, math.pi / 2.0), 

784 ] 

785 gaussBasisKernelList = makeGaussianKernelList( 

786 kWidth, kHeight, gaussParamsList) 

787 kernel = afwMath.LinearCombinationKernel( 

788 gaussBasisKernelList, spFunc) 

789 

790 numBasisKernels = kernel.getNKernelParameters() 

791 maxVal = 1.01 + ((numSpParams - 1) * 0.1) 

792 sParamList = [np.arange(kInd + 1.0, kInd + maxVal, 0.1) 

793 for kInd in range(numBasisKernels)] 

794 kernel.setSpatialParameters(sParamList) 

795 

796 refKernel = kernel.refactor() 

797 self.assertTrue(refKernel) 

798 errStr = self.compareKernels( 

799 kernel, refKernel, compareParams=False) 

800 if errStr: 

801 self.fail(f"failed with {errStr} for spOrder={spOrder}, numSpParams={numSpParams}") 

802 

803 def basicTests(self, kernel, nKernelParams, nSpatialParams=0, dimMustMatch=True): 

804 """Basic tests of a kernel""" 

805 self.assertEqual(kernel.getNSpatialParameters(), nSpatialParams) 

806 self.assertEqual(kernel.getNKernelParameters(), nKernelParams) 

807 if nSpatialParams == 0: 

808 self.assertFalse(kernel.isSpatiallyVarying()) 

809 for ii in range(nKernelParams+5): 

810 self.assertRaises(pexExcept.InvalidParameterError, 

811 kernel.getSpatialFunction, ii) 

812 else: 

813 self.assertTrue(kernel.isSpatiallyVarying()) 

814 for ii in range(nKernelParams): 

815 kernel.getSpatialFunction(ii) 

816 for ii in range(nKernelParams, nKernelParams+5): 

817 self.assertRaises(pexExcept.InvalidParameterError, 

818 kernel.getSpatialFunction, ii) 

819 

820 # test a range of numbers of parameters, including both valid and 

821 # invalid sized tuples. 

822 for nsp in range(nSpatialParams + 2): 

823 spatialParamsForOneKernel = [1.0]*nsp 

824 for nkp in range(nKernelParams + 2): 

825 spatialParams = [spatialParamsForOneKernel]*nkp 

826 if ((nkp == nKernelParams) and ((nsp == nSpatialParams) or (nkp == 0))): 

827 kernel.setSpatialParameters(spatialParams) 

828 if nsp == 0: 

829 # A non-spatially varying kernel returns an empty tuple, even though 

830 # it can only be set with a tuple of empty tuples, one 

831 # per kernel parameter. 

832 self.assertEqual(kernel.getSpatialParameters(), []) 

833 else: 

834 # a spatially varying kernel should return exactly what 

835 # we set it to be. 

836 self.assertEqual( 

837 kernel.getSpatialParameters(), spatialParams) 

838 else: 

839 with self.assertRaises(pexExcept.InvalidParameterError): 

840 kernel.setSpatialParameters(spatialParams) 

841 

842 kernelDim = kernel.getDimensions() 

843 kernelCtr = kernel.getCtr() 

844 for dx in (-1, 0, 1): 

845 xDim = kernelDim.getX() + dx 

846 for dy in (-1, 0, 1): 

847 if dx == dy == 0: 

848 continue 

849 yDim = kernelDim.getY() + dy 

850 image = afwImage.ImageD(xDim, yDim) 

851 if (dx == dy == 0) or not dimMustMatch: 

852 ksum = kernel.computeImage(image, True) 

853 self.assertAlmostEqual(ksum, 1.0) 

854 llBorder = ((image.getDimensions() - kernelDim) / 2).truncate() 

855 predCtr = lsst.geom.Point2I(llBorder + kernelCtr) 

856 self.assertEqual(kernel.getCtr(), predCtr) 

857 else: 

858 self.assertRaises( 

859 Exception, kernel.computeImage, image, True) 

860 

861 def basicTestComputeImageRaise(self, kernel, doRaise, kernelDescr=""): 

862 """Test that computeImage either does or does not raise an exception, as appropriate 

863 """ 

864 kImage = afwImage.ImageD(kernel.getDimensions()) 

865 try: 

866 kernel.computeImage(kImage, True) 

867 if doRaise: 

868 self.fail(f"{kernelDescr}.computeImage should have raised an exception") 

869 except pexExcept.Exception: 

870 if not doRaise: 

871 self.fail(f"{kernelDescr}.computeImage should not have raised an exception") 

872 

873 def compareResizedKernels(self, kernel1, kernel2): 

874 """Compare kernels' parameters and images where overlapping, 

875 ignorning sizes and centers. 

876 

877 return None if they match, else return a string describing 

878 all differences. 

879 """ 

880 retStrs = [] 

881 if kernel1.isSpatiallyVarying() != kernel2.isSpatiallyVarying(): 

882 retStrs.append("isSpatiallyVarying differs: " 

883 f"{kernel1.isSpatiallyVarying()} != {kernel2.isSpatiallyVarying()}") 

884 retStrs = self._compareParams(kernel1, kernel2, retStrs) 

885 if retStrs: 

886 return "; ".join(retStrs) 

887 

888 # New BBox; nothing is being modified. 

889 bboxIntersection = kernel1.getBBox() 

890 bboxIntersection.clip(kernel2.getBBox()) 

891 

892 im1 = afwImage.ImageD(kernel1.getDimensions()) 

893 im2 = afwImage.ImageD(kernel2.getDimensions()) 

894 posList = self._makePositionList(kernel1) 

895 

896 doNormalize = False 

897 for pos in posList: 

898 kernel1.computeImage(im1, doNormalize, pos[0], pos[1]) 

899 kernel2.computeImage(im2, doNormalize, pos[0], pos[1]) 

900 

901 im1Intersection = afwImage.ImageD(im1, bboxIntersection) 

902 im2Intersection = afwImage.ImageD(im2, bboxIntersection) 

903 

904 im1Arr = im1Intersection.getArray() 

905 im2Arr = im2Intersection.getArray() 

906 if not np.allclose(im1Arr, im2Arr): 

907 print("im1Arr =", im1Arr) 

908 print("im2Arr =", im2Arr) 

909 return f"kernel images do not match at {pos} with doNormalize={doNormalize}" 

910 

911 def compareKernels(self, kernel1, kernel2, compareParams=True, newCtr1=(0, 0)): 

912 """Compare two kernels; return None if they match, else return a string kernelDescribing a difference. 

913 

914 kernel1: one kernel to test 

915 kernel2: the other kernel to test 

916 compareParams: compare spatial parameters and kernel parameters if they exist 

917 newCtr: if not None then set the center of kernel1 and see if it changes the center of kernel2 

918 """ 

919 retStrs = [] 

920 if kernel1.getDimensions() != kernel2.getDimensions(): 

921 retStrs.append(f"dimensions differ: {kernel1.getDimensions()} != {kernel2.getDimensions()}") 

922 ctr1 = kernel1.getCtr() 

923 ctr2 = kernel2.getCtr() 

924 if ctr1 != ctr2: 

925 retStrs.append(f"centers differ: {ctr1} != {ctr2}") 

926 if kernel1.isSpatiallyVarying() != kernel2.isSpatiallyVarying(): 

927 retStrs.append("isSpatiallyVarying differs: " 

928 f"{kernel1.isSpatiallyVarying()} != {kernel2.isSpatiallyVarying()}") 

929 

930 if compareParams: 

931 retStrs = self._compareParams(kernel1, kernel2, retStrs) 

932 

933 if retStrs: 

934 return "; ".join(retStrs) 

935 

936 im1 = afwImage.ImageD(kernel1.getDimensions()) 

937 im2 = afwImage.ImageD(kernel2.getDimensions()) 

938 posList = self._makePositionList(kernel1) 

939 

940 for doNormalize in (False, True): 

941 for pos in posList: 

942 kernel1.computeImage(im1, doNormalize, pos[0], pos[1]) 

943 kernel2.computeImage(im2, doNormalize, pos[0], pos[1]) 

944 im1Arr = im1.getArray() 

945 im2Arr = im2.getArray() 

946 if not np.allclose(im1Arr, im2Arr): 

947 print("im1Arr =", im1Arr) 

948 print("im2Arr =", im2Arr) 

949 return "kernel images do not match at {pos} with doNormalize={doNormalize}" 

950 

951 if newCtr1 is not None: 

952 kernel1.setCtr(lsst.geom.Point2I(newCtr1)) 

953 newCtr2 = kernel2.getCtr() 

954 if ctr2 != newCtr2: 

955 return f"changing center of kernel1 to {newCtr1} " \ 

956 f"changed the center of kernel2 from {ctr2} to {newCtr2}" 

957 

958 def _compareParams(self, kernel1, kernel2, retStrs=None): 

959 """!Compare two kernels' Parameters""" 

960 if kernel1.getSpatialParameters() != kernel2.getSpatialParameters(): 

961 retStrs.append("spatial parameters differ: " 

962 f"{kernel1.getSpatialParameters()} != {kernel2.getSpatialParameters()}") 

963 if kernel1.getNSpatialParameters() != kernel2.getNSpatialParameters(): 

964 retStrs.append("# spatial parameters differs: " 

965 f"{kernel1.getNSpatialParameters()} != {kernel2.getNSpatialParameters()}") 

966 if not kernel1.isSpatiallyVarying() and hasattr(kernel1, "getKernelParameters"): 

967 if kernel1.getKernelParameters() != kernel2.getKernelParameters(): 

968 retStrs.append("kernel parameters differs: " 

969 f"{kernel1.getKernelParameters()} != {kernel2.getKernelParameters()}") 

970 return retStrs 

971 

972 def _makePositionList(self, kernel1): 

973 if kernel1.isSpatiallyVarying(): 

974 return [(0, 0), (200, 0), (0, 200), (200, 200)] 

975 else: 

976 return [(0, 0)] 

977 

978 

979class TestMemory(lsst.utils.tests.MemoryTestCase): 

980 pass 

981 

982 

983def setup_module(module): 

984 lsst.utils.tests.init() 

985 

986 

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

988 lsst.utils.tests.init() 

989 unittest.main()