Coverage for tests/test_kernel.py: 5%
604 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 01:19 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-01 01:19 -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
26import numpy as np
27from numpy.testing import assert_allclose
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
36def makeGaussianKernelList(kWidth, kHeight, gaussParamsList):
37 """Create a list of gaussian kernels.
39 This is useful for constructing a LinearCombinationKernel.
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
52def makeDeltaFunctionKernelList(kWidth, kHeight):
53 """Create a list of delta function kernels
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
65class KernelTestCase(lsst.utils.tests.TestCase):
66 """A test case for Kernels"""
68 def testAnalyticKernel(self):
69 """Test AnalyticKernel using a Gaussian function
70 """
71 kWidth = 5
72 kHeight = 8
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)
79 kernelResized = self.verifyResized(kernel)
80 self.basicTests(kernelResized, 3, dimMustMatch=False)
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()
95 kernel.setKernelParameters((xsigma, ysigma, 0.0))
96 kImage = afwImage.ImageD(kernel.getDimensions())
97 kernel.computeImage(kImage, True)
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}")
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)
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")
116 self.verifyCache(kernel, hasCache=False)
118 def verifyResized(self, kernel, testClip=True, oddPadRaises=False):
119 """Verify kernels can be resized properly.
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.
132 Returns
133 -------
134 kernelResized: `Kernel`
135 The last resized Kernel that was instantiated and tested
136 """
138 kWidth, kHeight = kernel.getDimensions()
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
145 if oddPadRaises:
146 badDims.extend([(kWidth + w, kHeight + h) for (w, h) in oddPads])
147 else:
148 goodPads.extend(oddPads)
150 if testClip:
151 goodPads.extend(clippingPads)
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)
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)
171 return kernelResized
173 def verifyCache(self, kernel, hasCache=False):
174 """Verify the kernel cache
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())
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
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")
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)
232 errStr = self.compareKernels(kernel, kernel.clone())
233 if errStr:
234 self.fail(errStr)
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))
241 kernel = afwMath.DeltaFunctionKernel(5, 6, lsst.geom.Point2I(1, 1))
242 self.basicTests(kernel, 0)
244 kernelResized = self.verifyResized(kernel, oddPadRaises=True)
245 self.basicTests(kernelResized, 0)
246 self.verifyCache(kernel, hasCache=False)
248 def testFixedKernel(self):
249 """Test FixedKernel using a ramp function
250 """
251 kWidth = 5
252 kHeight = 6
254 inArr = np.arange(kWidth * kHeight, dtype=float)
255 inArr.shape = [kWidth, kHeight]
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]
262 kernel = afwMath.FixedKernel(inImage)
263 self.basicTests(kernel, 0)
265 kernelResized = self.verifyResized(kernel, oddPadRaises=True)
266 self.basicTests(kernelResized, 0)
268 outImage = afwImage.ImageD(kernel.getDimensions())
269 kernel.computeImage(outImage, False)
271 outArr = outImage.getArray().transpose()
272 if not np.allclose(inArr, outArr):
273 self.fail(f"{kernel.__class__.__name__} = {inArr} != {outArr} (not normalized)")
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)")
282 errStr = self.compareKernels(kernel, kernel.clone())
283 if errStr:
284 self.fail(errStr)
286 self.verifyCache(kernel, hasCache=False)
288 def testLinearCombinationKernelDelta(self):
289 """Test LinearCombinationKernel using a set of delta basis functions
290 """
291 kWidth = 3
292 kHeight = 2
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())
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")
319 kernelClone = kernel.clone()
320 errStr = self.compareKernels(kernel, kernelClone)
321 if errStr:
322 self.fail(errStr)
324 self.verifyCache(kernel, hasCache=False)
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
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])
339 analKernel.computeImage(kImage, False)
340 fixedKernel = afwMath.FixedKernel(kImage)
342 sepKernel = afwMath.SeparableKernel(
343 kWidth, kHeight, polyFunc1, polyFunc1)
344 sepKernel.setKernelParameters([coeff, coeff])
346 kernelList = []
347 kernelList.append(analKernel)
348 lcKernel = afwMath.LinearCombinationKernel(kernelList, [1])
349 self.assertFalse(lcKernel.isDeltaFunctionBasis())
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")
361 lcKernel.setKernelParameters([0])
362 self.basicTestComputeImageRaise(
363 lcKernel, True, "LinearCombinationKernel")
365 def testLinearCombinationKernelAnalytic(self):
366 """Test LinearCombinationKernel using analytic basis kernels.
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
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)
387 kParams = [0.0]*len(basisKernelList)
388 kernel = afwMath.LinearCombinationKernel(basisKernelList, kParams)
389 self.assertTrue(not kernel.isDeltaFunctionBasis())
390 self.basicTests(kernel, len(kParams))
392 kernelResized = self.verifyResized(kernel)
393 self.basicTests(kernelResized, len(kParams))
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())
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")
419 kernelClone = kernel.clone()
420 errStr = self.compareKernels(kernel, kernelClone)
421 if errStr:
422 self.fail(errStr)
424 self.verifyCache(kernel, hasCache=False)
426 def testSeparableKernel(self):
427 """Test SeparableKernel using a Gaussian function
428 """
429 kWidth = 5
430 kHeight = 8
432 gaussFunc1 = afwMath.GaussianFunction1D(1.0)
433 kernel = afwMath.SeparableKernel(
434 kWidth, kHeight, gaussFunc1, gaussFunc1)
435 center = kernel.getCtr()
436 self.basicTests(kernel, 2)
438 kernelResized = self.verifyResized(kernel)
439 self.basicTests(kernelResized, 2)
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()
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)
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")
474 self.verifyCache(kernel, hasCache=True)
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
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)))
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
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
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
534 def testSVAnalyticKernel(self):
535 """Test spatially varying AnalyticKernel using a Gaussian function
537 Just tests cloning.
538 """
539 kWidth = 5
540 kHeight = 8
542 # spatial model
543 spFunc = afwMath.PolynomialFunction2D(1)
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 )
553 gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0)
554 kernel = afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc, spFunc)
555 kernel.setSpatialParameters(sParams)
557 kernelClone = kernel.clone()
558 errStr = self.compareKernels(kernel, kernelClone)
559 if errStr:
560 self.fail(errStr)
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")
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))
579 self.assertTrue(re.search("AnalyticKernel", kernel.toString()))
580 self.assertFalse(kernel2.isSpatiallyVarying())
582 self.assertTrue(re.search("FixedKernel", kernel2.toString()))
583 self.assertTrue(kernel.isSpatiallyVarying())
585 kim = afwImage.ImageD(kernel.getDimensions())
586 kernel.computeImage(kim, True, x, y)
588 kim2 = afwImage.ImageD(kernel2.getDimensions())
589 kernel2.computeImage(kim2, True)
591 assert_allclose(kim.getArray(), kim2.getArray())
593 def testSVLinearCombinationKernelFixed(self):
594 """Test a spatially varying LinearCombinationKernel whose bases are FixedKernels"""
595 kWidth = 3
596 kHeight = 2
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)
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)
617 # create spatially varying linear combination kernel
618 spFunc = afwMath.PolynomialFunction2D(1)
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 )
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}")
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)
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")
667 def testSVSeparableKernel(self):
668 """Test spatially varying SeparableKernel using a Gaussian function
670 Just tests cloning.
671 """
672 kWidth = 5
673 kHeight = 8
675 # spatial model
676 spFunc = afwMath.PolynomialFunction2D(1)
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 )
685 gaussFunc = afwMath.GaussianFunction1D(1.0)
686 kernel = afwMath.SeparableKernel(
687 kWidth, kHeight, gaussFunc, gaussFunc, spFunc)
688 kernel.setSpatialParameters(sParams)
690 kernelClone = kernel.clone()
691 errStr = self.compareKernels(kernel, kernelClone)
692 if errStr:
693 self.fail(errStr)
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")
705 def testSetCtr(self):
706 """Test setCtrCol/Row"""
707 kWidth = 3
708 kHeight = 4
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)
718 def testZeroSizeKernel(self):
719 """Creating a kernel with width or height < 1 should raise an exception.
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)
744 def testRefactorDeltaLinearCombinationKernel(self):
745 """Test LinearCombinationKernel.refactor with delta function basis kernels
746 """
747 kWidth = 4
748 kHeight = 3
750 for spOrder in (0, 1, 2):
751 spFunc = afwMath.PolynomialFunction2D(spOrder)
752 numSpParams = spFunc.getNParameters()
754 basisKernelList = makeDeltaFunctionKernelList(kWidth, kHeight)
755 kernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
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)
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}")
770 def testRefactorGaussianLinearCombinationKernel(self):
771 """Test LinearCombinationKernel.refactor with Gaussian basis kernels
772 """
773 kWidth = 4
774 kHeight = 3
776 for spOrder in (0, 1, 2):
777 spFunc = afwMath.PolynomialFunction2D(spOrder)
778 numSpParams = spFunc.getNParameters()
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)
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)
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}")
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)
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)
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)
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")
873 def compareResizedKernels(self, kernel1, kernel2):
874 """Compare kernels' parameters and images where overlapping,
875 ignorning sizes and centers.
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)
888 # New BBox; nothing is being modified.
889 bboxIntersection = kernel1.getBBox()
890 bboxIntersection.clip(kernel2.getBBox())
892 im1 = afwImage.ImageD(kernel1.getDimensions())
893 im2 = afwImage.ImageD(kernel2.getDimensions())
894 posList = self._makePositionList(kernel1)
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])
901 im1Intersection = afwImage.ImageD(im1, bboxIntersection)
902 im2Intersection = afwImage.ImageD(im2, bboxIntersection)
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}"
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.
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()}")
930 if compareParams:
931 retStrs = self._compareParams(kernel1, kernel2, retStrs)
933 if retStrs:
934 return "; ".join(retStrs)
936 im1 = afwImage.ImageD(kernel1.getDimensions())
937 im2 = afwImage.ImageD(kernel2.getDimensions())
938 posList = self._makePositionList(kernel1)
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}"
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}"
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
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)]
979class TestMemory(lsst.utils.tests.MemoryTestCase):
980 pass
983def setup_module(module):
984 lsst.utils.tests.init()
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()