Coverage for tests/test_function.py: 6%
470 statements
« prev ^ index » next coverage.py v6.4, created at 2022-06-02 03:42 -0700
« prev ^ index » next coverage.py v6.4, created at 2022-06-02 03:42 -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#
23import itertools
24import math
25import unittest
27import numpy as np
29import lsst.utils.tests
30import lsst.geom
31import lsst.afw.math as afwMath
32import lsst.pex.exceptions as pexExceptions
35def nrange(num, start, delta):
36 """Return an array of num floats starting with start and incrementing by delta
37 """
38 return np.arange(start, start + (delta * (num - 0.1)), delta)
41def sinc(x):
42 """Return the normalized sinc function: sinc(x) = sin(pi * x) / (pi * x)
43 """
44 if abs(x) < 1.0e-15:
45 return 1.0
46 return math.sin(math.pi * x) / (math.pi * x)
49def referenceChebyshev1(x, n):
50 """Reference implementation of Chebyshev polynomials of the first kind
52 f(x) = T_n(x)
53 """
54 # from Wikipedia
55 if n == 0:
56 return 1.0
57 if n == 1:
58 return x
59 return (2.0 * x * referenceChebyshev1(x, n-1)) - referenceChebyshev1(x, n-2)
62def referenceChebyshev1Polynomial1(x, params):
63 """Reference implementation of a 1-D polynomial of Chebyshev polynomials of the first kind
65 f(x) = params[0] T_0(x) + params[1] T_1(x) + params[2] T_2(x)
66 """
67 retVal = 0.0
68 for ii in range(len(params)-1, -1, -1):
69 retVal += params[ii] * referenceChebyshev1(x, ii)
70 return retVal
73def referenceChebyshev1Polynomial2(x, y, params):
74 """Reference implementation of a 2-D polynomial of Chebyshev polynomials of the first kind
76 f(x) = params[0] T_0(x) T_0(y) # order 0
77 + params[1] T_1(x) T_0(y) + params[2] T_0(x) T_1(y) # order 1
78 + params[3] T_2(x) T_0(y) + params[4] T_1(x) T_1(y) + params[5] T_0(x) T_2(y) # order 2
79 + ...
81 Raise RuntimeError if the number of parameters does not match an integer order.
82 """
83 retVal = 0.0
84 order = 0
85 y_order = 0
86 for ii in range(0, len(params)):
87 x_order = order - y_order
88 retVal += params[ii] * \
89 referenceChebyshev1(x, x_order) * referenceChebyshev1(y, y_order)
90 if x_order > 0:
91 y_order += 1
92 else:
93 order += 1
94 y_order = 0
95 if y_order != 0:
96 raise RuntimeError(f"invalid # of parameters={len(params)}")
97 return retVal
100class FunctionTestCase(lsst.utils.tests.TestCase):
101 def setUp(self):
102 self.normErr = "Invalid {0} normalization: min={1}, max={2}, min/max norm=({3}, {4}) != (-1, 1)"
103 # We need a slightly larger than the full floating point tolerance for
104 # many of these tests.
105 self.atol = 5e-14
107 def testChebyshev1Function1D(self):
108 errMsg = "{}: {} != {} for x={}, xMin={}, xMax={}, xNorm={}, params={}; {}"
109 maxOrder = 6
110 deltaParam = 0.3
111 ranges = ((-1, 1), (-1, 0), (0, 1), (-17, -2), (-65.3, 2.132))
112 rangeIter = itertools.cycle(ranges)
113 nPoints = 9
115 for order in range(maxOrder + 1):
116 xMin, xMax = next(rangeIter)
117 xMean = (xMin + xMax) / 2.0
118 xDelta = (xMax - xMin) / float(nPoints - 1)
120 f = afwMath.Chebyshev1Function1D(order, xMin, xMax)
121 numParams = f.getNParameters()
122 params = np.arange(deltaParam, deltaParam*numParams + (deltaParam / 2.0), deltaParam)
123 f.setParameters(params)
124 g = afwMath.Chebyshev1Function1D(params, xMin, xMax)
125 h = f.clone()
127 self.assertEqual(f.getNParameters(), g.getNParameters())
129 self.assertEqual(f.getMinX(), xMin)
130 self.assertEqual(f.getMaxX(), xMax)
131 self.assertEqual(f.getOrder(), order)
133 self.assertEqual(g.getMinX(), xMin)
134 self.assertEqual(g.getMaxX(), xMax)
135 self.assertEqual(g.getOrder(), order)
137 minXNorm = None
138 maxXNorm = None
139 for x in np.arange(xMin, xMax + xDelta/2.0, xDelta):
140 xNorm = 2.0 * (x - xMean) / float(xMax - xMin)
141 if minXNorm is None or xNorm < minXNorm:
142 minXNorm = xNorm
143 if maxXNorm is None or xNorm > maxXNorm:
144 maxXNorm = xNorm
146 predVal = referenceChebyshev1Polynomial1(xNorm, params)
147 msg = errMsg.format(type(f).__name__, f(x), predVal, x, xMin, xMax, xNorm, params,
148 "order constructor")
149 self.assertFloatsAlmostEqual(
150 f(x), predVal, msg=msg, atol=self.atol)
151 msg = errMsg.format(type(g).__name__, g(x), predVal, x, xMin, xMax, xNorm, params,
152 "params constructor")
153 self.assertFloatsAlmostEqual(
154 g(x), predVal, msg=msg, atol=self.atol)
155 msg = errMsg.format(type(h).__name__, h(x),
156 predVal, x, xMin, xMax, xNorm, params,
157 "clone")
158 self.assertFloatsAlmostEqual(
159 h(x), predVal, msg=msg, atol=self.atol)
161 msg = self.normErr.format("x", xMin, xMax, minXNorm, maxXNorm)
162 self.assertFloatsAlmostEqual(
163 minXNorm, -1., msg=msg, atol=self.atol)
164 self.assertFloatsAlmostEqual(maxXNorm, 1., msg=msg, atol=self.atol)
166 def testChebyshev1Function2D(self):
167 errMsg = ("{}: {} != {} for x={}, xMin={}, xMax={}, xNorm={}, "
168 "yMin={}, yMax={}, yNorm={}, params={}; {}")
169 maxOrder = 6
170 deltaParam = 0.3
171 ranges = ((-1, 1), (-1, 0), (0, 1), (-17, -2), (-65.3, 2.132))
172 xRangeIter = itertools.cycle(ranges)
173 yRangeIter = itertools.cycle(ranges)
174 next(yRangeIter) # make x and y ranges off from each other
175 nPoints = 7 # number of points in x and y at which to test the functions
177 for order in range(maxOrder + 1):
178 xMin, xMax = next(xRangeIter)
179 xMean = (xMin + xMax) / 2.0
180 xDelta = (xMax - xMin) / float(nPoints - 1)
182 yMin, yMax = next(yRangeIter)
183 yMean = (yMin + yMax) / 2.0
184 yDelta = (yMax - yMin) / float(nPoints - 1)
186 xyRange = lsst.geom.Box2D(lsst.geom.Point2D(xMin, yMin),
187 lsst.geom.Point2D(xMax, yMax))
189 f = afwMath.Chebyshev1Function2D(order, xyRange)
190 numParams = f.getNParameters()
191 params = nrange(numParams, deltaParam, deltaParam)
192 f.setParameters(params)
193 g = afwMath.Chebyshev1Function2D(params, xyRange)
194 h = f.clone()
196 self.assertEqual(f.getNParameters(), g.getNParameters())
197 self.assertEqual(f.getNParameters(), h.getNParameters())
199 self.assertEqual(f.getXYRange(), xyRange)
200 self.assertEqual(f.getOrder(), order)
202 self.assertEqual(g.getXYRange(), xyRange)
203 self.assertEqual(g.getOrder(), order)
205 # vary x in the inner loop to exercise the caching
206 minYNorm = None
207 maxYNorm = None
208 for y in np.arange(yMin, yMax + yDelta/2.0, yDelta):
209 yNorm = 2.0 * (y - yMean) / float(yMax - yMin)
210 if minYNorm is None or yNorm < minYNorm:
211 minYNorm = yNorm
212 if maxYNorm is None or yNorm > maxYNorm:
213 maxYNorm = yNorm
215 minXNorm = None
216 maxXNorm = None
217 for x in np.arange(xMin, xMax + xDelta/2.0, xDelta):
218 xNorm = 2.0 * (x - xMean) / float(xMax - xMin)
219 if minXNorm is None or xNorm < minXNorm:
220 minXNorm = xNorm
221 if maxXNorm is None or xNorm > maxXNorm:
222 maxXNorm = xNorm
224 predVal = referenceChebyshev1Polynomial2(
225 xNorm, yNorm, params)
227 msg = errMsg.format(type(f).__name__, f(x, y), predVal, x, xMin, xMax, xNorm,
228 yMin, yMax, yNorm, params, "order constructor")
229 self.assertFloatsAlmostEqual(
230 f(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
231 msg = errMsg.format(type(g).__name__, g(x, y), predVal, x, xMin, xMax, xNorm,
232 yMin, yMax, yNorm, params, "params constructor")
233 self.assertFloatsAlmostEqual(
234 g(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
235 msg = errMsg.format(type(h).__name__, h(x, y), predVal, x, xMin, xMax, xNorm,
236 yMin, yMax, yNorm, params, "order")
237 self.assertFloatsAlmostEqual(
238 h(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
240 msg = self.normErr.format("x", xMin, xMax, minXNorm, maxXNorm)
241 self.assertFloatsAlmostEqual(
242 minXNorm, -1., msg=msg, atol=self.atol)
243 self.assertFloatsAlmostEqual(
244 maxXNorm, 1., msg=msg, atol=self.atol)
246 msg = self.normErr.format("y", yMin, yMax, minYNorm, maxYNorm)
247 self.assertFloatsAlmostEqual(
248 minYNorm, -1., msg=msg, atol=self.atol)
249 self.assertFloatsAlmostEqual(maxYNorm, 1., msg=msg, atol=self.atol)
251 # test that the number of parameters is correct for the given order
252 def numParamsFromOrder(order):
253 return (order + 1) * (order + 2) // 2
254 MaxOrder = 13
255 for order in range(MaxOrder+1):
256 f = afwMath.Chebyshev1Function2D(order)
257 predNParams = numParamsFromOrder(order)
258 self.assertEqual(f.getNParameters(), predNParams)
259 afwMath.Chebyshev1Function2D(np.zeros(predNParams, dtype=float))
261 # test that the wrong number of parameters raises an exception
262 validNumParams = set()
263 for order in range(MaxOrder+1):
264 validNumParams.add(numParamsFromOrder(order))
265 for numParams in range(numParamsFromOrder(MaxOrder)):
266 if numParams in validNumParams:
267 continue
268 with self.assertRaises(pexExceptions.InvalidParameterError):
269 afwMath.Chebyshev1Function2D(np.zeros(numParams, dtype=float))
271 # test that changing parameters clears the cache
272 # for simplicity use the xyRange that requires no normalization
273 order = 3
274 numParams = numParamsFromOrder(order)
275 f = afwMath.Chebyshev1Function2D(order)
276 xyRange = lsst.geom.Box2D(
277 lsst.geom.Point2D(-1.0, -1.0), lsst.geom.Point2D(1.0, 1.0))
278 x = 0.5
279 y = -0.24
280 for addValue in (0.0, 0.2):
281 params = nrange(numParams, deltaParam + addValue, deltaParam)
282 f.setParameters(params)
283 predVal = referenceChebyshev1Polynomial2(x, y, params)
284 msg = f"{f(x, y)} != {predVal} for x={x}, y={y}, params={params}"
285 self.assertFloatsAlmostEqual(
286 f(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
288 def testChebyshev1Function2DTruncate(self):
289 errMsg = ("{} != {} = {} for x={}, xMin={}, xMax={}, xNorm={},"
290 " yMin={}, yMax={}, yNorm={}, truncParams={}; order constructor")
292 maxOrder = 6
293 deltaParam = 0.3
294 ranges = ((-1, 1), (-17, -2), (-65.3, 2.132))
295 xRangeIter = itertools.cycle(ranges)
296 yRangeIter = itertools.cycle(ranges)
297 next(yRangeIter) # make x and y ranges off from each other
298 nPoints = 7 # number of points in x and y at which to test the functions
300 for order in range(maxOrder + 1):
301 xMin, xMax = next(xRangeIter)
302 xMean = (xMin + xMax) / 2.0
303 xDelta = (xMax - xMin) / float(nPoints - 1)
305 yMin, yMax = next(yRangeIter)
306 yMean = (yMin + yMax) / 2.0
307 yDelta = (yMax - yMin) / float(nPoints - 1)
309 xyRange = lsst.geom.Box2D(lsst.geom.Point2D(xMin, yMin),
310 lsst.geom.Point2D(xMax, yMax))
312 fullNParams = afwMath.Chebyshev1Function2D.nParametersFromOrder(
313 order)
314 fullParams = nrange(fullNParams, deltaParam, deltaParam)
315 fullPoly = afwMath.Chebyshev1Function2D(fullParams, xyRange)
317 for tooBigTruncOrder in range(order + 1, order + 3):
318 with self.assertRaises(pexExceptions.InvalidParameterError):
319 fullPoly.truncate(tooBigTruncOrder)
321 for truncOrder in range(order + 1):
322 truncNParams = fullPoly.nParametersFromOrder(truncOrder)
323 truncParams = fullParams[0:truncNParams]
325 f = fullPoly.truncate(truncOrder)
326 self.assertEqual(f.getNParameters(), truncNParams)
328 g = afwMath.Chebyshev1Function2D(
329 fullParams[0:truncNParams], xyRange)
331 self.assertEqual(f.getNParameters(), g.getNParameters())
333 self.assertEqual(f.getOrder(), truncOrder)
334 self.assertEqual(f.getXYRange(), xyRange)
336 self.assertEqual(g.getOrder(), truncOrder)
337 self.assertEqual(g.getXYRange(), xyRange)
339 minXNorm = None
340 maxXNorm = None
341 for x in np.arange(xMin, xMax + xDelta/2.0, xDelta):
342 xNorm = 2.0 * (x - xMean) / float(xMax - xMin)
343 if minXNorm is None or xNorm < minXNorm:
344 minXNorm = xNorm
345 if maxXNorm is None or xNorm > maxXNorm:
346 maxXNorm = xNorm
348 minYNorm = None
349 maxYNorm = None
350 for y in np.arange(yMin, yMax + yDelta/2.0, yDelta):
351 yNorm = 2.0 * (y - yMean) / float(yMax - yMin)
352 if minYNorm is None or yNorm < minYNorm:
353 minYNorm = yNorm
354 if maxYNorm is None or yNorm > maxYNorm:
355 maxYNorm = yNorm
357 msg = errMsg.format(type(f).__name__, f(x, y), g(x, y), type(g).__name__,
358 x, xMin, xMax, xNorm, yMin, yMax, yNorm, truncParams)
359 self.assertFloatsAlmostEqual(
360 f(x, y), g(x, y), msg=msg)
362 msg = self.normErr.format(
363 "y", yMin, yMax, minYNorm, maxYNorm)
364 self.assertFloatsAlmostEqual(
365 minYNorm, -1.0, msg=msg, atol=self.atol, rtol=None)
366 self.assertFloatsAlmostEqual(
367 maxYNorm, 1.0, msg=msg, atol=self.atol, rtol=None)
369 msg = self.normErr.format("x", xMin, xMax, minXNorm, maxXNorm)
370 self.assertFloatsAlmostEqual(
371 minXNorm, -1.0, msg=msg, atol=self.atol, rtol=None)
372 self.assertFloatsAlmostEqual(
373 maxXNorm, 1.0, msg=msg, atol=self.atol, rtol=None)
375 def testGaussianFunction1D(self):
376 def basicGaussian(x, sigma):
377 return (1.0 / (sigma * math.sqrt(2 * math.pi))) * math.exp(-x**2 / (2.0 * sigma**2))
379 f = afwMath.GaussianFunction1D(1.0)
380 for xsigma in (0.1, 1.0, 3.0):
381 f.setParameters((xsigma,))
382 g = f.clone()
383 xdelta = xsigma / 10.0
384 fSum = 0.0
385 for x in np.arange(-xsigma * 20, xsigma * 20.01, xdelta):
386 predVal = basicGaussian(x, xsigma)
387 fSum += predVal
388 msg = f"{type(f).__name__} = {f(x)} != {predVal} for x={x}, xsigma={xsigma}"
389 self.assertFloatsAlmostEqual(
390 f(x), predVal, msg=msg, atol=self.atol, rtol=None)
391 msg += "; clone"
392 self.assertFloatsAlmostEqual(
393 g(x), predVal, msg=msg, atol=self.atol, rtol=None)
394 approxArea = fSum * xdelta
395 msg = f"{type(f).__name__} area = {approxArea} != 1.0 for xsigma={xsigma}"
396 self.assertFloatsAlmostEqual(
397 approxArea, 1.0, msg=msg, atol=self.atol, rtol=None)
399 def testGaussianFunction2D(self):
400 """Note: Assumes GaussianFunction1D is correct (tested elsewhere)."""
401 errMsg = "{} = {} != {} for pos1={}, pos2={}, x={}, y={}, sigma1={}, sigma2={}, angle={}"
402 areaMsg = "%s area = %s != 1.0 for sigma1=%s, sigma2=%s"
403 f = afwMath.GaussianFunction2D(1.0, 1.0, 0.0)
404 f1 = afwMath.GaussianFunction1D(1.0)
405 f2 = afwMath.GaussianFunction1D(1.0)
406 for sigma1 in (0.1, 1.0, 3.0):
407 for sigma2 in (0.1, 1.0, 3.0):
408 for angle in (0.0, 0.4, 1.1):
409 sinNegAngle = math.sin(-angle)
410 cosNegAngle = math.cos(-angle)
411 f.setParameters((sigma1, sigma2, angle))
412 g = f.clone()
413 f1.setParameters((sigma1,))
414 f2.setParameters((sigma2,))
415 fSum = 0.0
416 delta1 = sigma1 / 5.0
417 delta2 = sigma2 / 5.0
418 for pos1 in np.arange(-sigma1 * 5, sigma1 * 5.01, delta1):
419 for pos2 in np.arange(-sigma2 * 5.0, sigma2 * 5.01, delta2):
420 x = (cosNegAngle * pos1) + (sinNegAngle * pos2)
421 y = (-sinNegAngle * pos1) + (cosNegAngle * pos2)
422 predVal = f1(pos1) * f2(pos2)
423 fSum += predVal
424 msg = errMsg.format(type(f).__name__, f(x, y), predVal,
425 pos1, pos2, x, y, sigma1, sigma2, angle)
426 self.assertFloatsAlmostEqual(
427 f(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
428 msg = errMsg.format(type(g).__name__, g(x, y), predVal,
429 pos1, pos2, x, y, sigma1, sigma2, angle) + "; clone"
430 self.assertFloatsAlmostEqual(
431 g(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
432 approxArea = fSum * delta1 * delta2
433 msg = areaMsg % (type(f).__name__,
434 approxArea, sigma1, sigma2)
435 # approxArea is very approximate, so we need a high
436 # tolerance threshold.
437 self.assertFloatsAlmostEqual(
438 approxArea, 1.0, msg=msg, atol=1e-6, rtol=None)
440 def testDoubleGaussianFunction2D(self):
441 """Note: Assumes GaussianFunction2D is correct (tested elsewhere)."""
442 errMsg = "{} = {} != {} for x={}, y={}, sigma1={}, sigma2={}, b={}"
443 areaMsg = "{} area = {} != 1.0 for sigma1={}, sigma2={}"
444 f = afwMath.DoubleGaussianFunction2D(1.0, 1.0)
445 f1 = afwMath.GaussianFunction2D(1.0, 1.0, 0.0)
446 f2 = afwMath.GaussianFunction2D(1.0, 1.0, 0.0)
447 for sigma1 in (1.0,):
448 for sigma2 in (0.5, 2.0):
449 for b in (0.0, 0.2, 2.0):
450 f.setParameters((sigma1, sigma2, b))
451 g = f.clone()
452 f1.setParameters((sigma1, sigma1, 0.0))
453 f2.setParameters((sigma2, sigma2, 0.0))
454 sigma1Sq = sigma1**2
455 sigma2Sq = sigma2**2
456 f1Mult = b * sigma2Sq / sigma1Sq
457 allMult = sigma1Sq / (sigma1Sq + (b * sigma2Sq))
458 fSum = 0.0
459 maxsigma = max(sigma1, sigma2)
460 minsigma = min(sigma1, sigma2)
461 delta = minsigma / 5.0
462 for y in np.arange(-maxsigma * 5, maxsigma * 5.01, delta):
463 for x in np.arange(-maxsigma * 5.0, maxsigma * 5.01, delta):
464 predVal = (
465 f1(x, y) + (f1Mult * f2(x, y))) * allMult
466 fSum += predVal
467 msg = errMsg.format(
468 type(f).__name__,
469 f(x, y), predVal, x, y, sigma1, sigma2, b)
470 self.assertFloatsAlmostEqual(
471 f(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
472 msg = errMsg.format(type(g).__name__, g(x, y), predVal,
473 x, y, sigma1, sigma2, b) + "; clone"
474 self.assertFloatsAlmostEqual(
475 g(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
476 approxArea = fSum * delta**2
477 msg = areaMsg.format(
478 type(f).__name__, approxArea, sigma1, sigma2)
479 # approxArea is very approximate, so we need a high
480 # tolerance threshold.
481 self.assertFloatsAlmostEqual(
482 approxArea, 1.0, msg=msg, atol=1e-6, rtol=None)
484 def testIntegerDeltaFunction2D(self):
485 def basicDelta(x, xo):
486 return (x == xo)
488 errMsg = "{} = {} != {} for x={}, y={}, xo={}, yo={}"
489 for xo in np.arange(-5.0, 5.0, 1.0):
490 for yo in np.arange(-5.0, 5.0, 1.0):
491 f = afwMath.IntegerDeltaFunction2D(xo, yo)
492 g = f.clone()
493 for x in np.arange(-5.0, 5.0, 1.0):
494 for y in np.arange(-5.0, 5.0, 1.0):
495 predVal = basicDelta(x, xo) * basicDelta(y, yo)
496 msg = errMsg.format(type(f).__name__,
497 f(x, y), predVal, x, y, xo, yo)
498 self.assertFloatsAlmostEqual(
499 f(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
500 msg = errMsg.format(type(g).__name__,
501 g(x, y), predVal, x, y, xo, yo) + "; clone"
502 self.assertFloatsAlmostEqual(
503 g(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
505 def testLanczosFunction1D(self):
506 def basicLanczos1(x, n):
507 return sinc(x) * sinc(x / float(n))
509 errMsg = "{} = {} != {} for n={}, x={}, xOffset={}, xAdj={}"
510 for n in range(1, 5):
511 f = afwMath.LanczosFunction1D(n)
512 self.assertEqual(f.getOrder(), n)
514 for xOffset in (-10.0, 0.0, 0.05):
515 f.setParameters((xOffset,))
516 g = f.clone()
517 for x in np.arange(-10.0, 10.1, 0.50):
518 xAdj = x - xOffset
519 predVal = basicLanczos1(xAdj, n)
520 msg = errMsg.format(type(f).__name__,
521 f(x), predVal, n, x, xOffset, xAdj)
522 self.assertFloatsAlmostEqual(
523 f(x), predVal, msg=msg, atol=self.atol, rtol=None)
524 msg = errMsg.format(type(g).__name__,
525 g(x), predVal, n, x, xOffset, xAdj) + "; clone"
526 self.assertFloatsAlmostEqual(
527 g(x), predVal, msg=msg, atol=self.atol, rtol=None)
529 def testLanczosFunction2D(self):
530 def basicLanczos1(x, n):
531 return sinc(x) * sinc(x / float(n))
533 errMsg = "{} = {} != {} for n={}, x={}, xOffset={}, yOffset={}, xAdj={}, yAdj={}"
534 for n in range(1, 5):
535 f = afwMath.LanczosFunction2D(n)
536 self.assertEqual(f.getOrder(), n)
538 for xOffset in (-10.0, 0.0, 0.05):
539 for yOffset in (-0.01, 0.0, 7.5):
540 f.setParameters((xOffset, yOffset))
541 g = f.clone()
542 for x in np.arange(-10.0, 10.1, 2.0):
543 for y in np.arange(-10.0, 10.1, 2.0):
544 xAdj = x - xOffset
545 yAdj = y - yOffset
546 predVal = basicLanczos1(xAdj, n) * \
547 basicLanczos1(yAdj, n)
548 msg = errMsg.format(type(f).__name__, f(x, y), predVal, n,
549 x, xOffset, yOffset, xAdj, yAdj)
550 self.assertFloatsAlmostEqual(
551 f(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
552 msg = errMsg.format(type(g).__name__, g(x, y), predVal, n,
553 x, xOffset, yOffset, xAdj, yAdj)
554 self.assertFloatsAlmostEqual(
555 g(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
557 def testPolynomialFunction1D(self):
558 def basic1DPoly(x, params):
559 ii = len(params) - 1
560 retVal = params[ii]
561 while ii > 0:
562 ii -= 1
563 retVal = retVal * x + params[ii]
564 return retVal
566 maxOrder = 4
567 deltaParam = 0.3
568 errMsg = "{} = {} != {} for x={}, params={}; {}"
570 # test value using order constructor
571 for order in range(maxOrder):
572 numParams = order + 1
573 params = nrange(numParams, deltaParam, deltaParam)
574 f = afwMath.PolynomialFunction1D(params)
575 g = afwMath.PolynomialFunction1D(order)
576 g.setParameters(params)
577 h = f.clone()
579 self.assertEqual(f.getOrder(), order)
580 self.assertEqual(g.getOrder(), order)
582 for x in np.arange(-10.0, 10.1, 1.0):
583 predVal = basic1DPoly(x, params)
584 msg = errMsg.format(type(f).__name__,
585 f(x), predVal, x, params,
586 "params constructor")
587 self.assertFloatsAlmostEqual(
588 f(x), predVal, msg=msg, atol=self.atol, rtol=1E-13)
589 msg = errMsg.format(type(g).__name__,
590 g(x), predVal, x, params,
591 "order constructor")
592 self.assertFloatsAlmostEqual(
593 g(x), predVal, msg=msg, atol=self.atol, rtol=1E-13)
594 msg = errMsg.format(type(h).__name__,
595 h(x), predVal, x, params, "clone")
596 self.assertFloatsAlmostEqual(
597 h(x), predVal, msg=msg, atol=self.atol, rtol=1E-13)
599 def testPolynomialFunction2D(self):
600 def basic2DPoly(x, y, params):
601 retVal = 0
602 numParams = len(params)
603 order = 0
604 ii = 0
605 while True:
606 for yOrder in range(order+1):
607 xOrder = order - yOrder
608 retVal += params[ii] * x**xOrder * y**yOrder
609 ii += 1
610 if ii >= numParams:
611 if xOrder != 0:
612 raise RuntimeError(f"invalid # params={numParams}")
613 return retVal
614 order += 1
616 numParamsList = (1, 3, 6, 10)
617 deltaParam = 0.3
618 errMsg = "{} = {} != {} for x={}, y={}, params={}; {}"
620 # test function values
621 for order, numParams in enumerate(numParamsList):
622 params = nrange(numParams, deltaParam, deltaParam)
623 f = afwMath.PolynomialFunction2D(params)
624 g = afwMath.PolynomialFunction2D(order)
625 g.setParameters(params)
626 h = f.clone()
628 self.assertEqual(f.getOrder(), order)
629 self.assertEqual(g.getOrder(), order)
631 # vary x in the inner loop to exercise the caching
632 for y in np.arange(-10.0, 10.1, 2.5):
633 for x in np.arange(-10.0, 10.1, 2.5):
634 predVal = basic2DPoly(x, y, params)
635 msg = errMsg.format(type(f).__name__, f(x, y), predVal,
636 x, y, params, "params constructor")
637 self.assertFloatsAlmostEqual(
638 f(x, y), predVal, msg=msg, atol=2e-12, rtol=None)
639 msg = errMsg.format(type(g).__name__,
640 g(x, y), predVal, x, y, params,
641 "order constructor")
642 self.assertFloatsAlmostEqual(
643 g(x, y), predVal, msg=msg, atol=2e-12, rtol=None)
644 msg = errMsg.format(type(h).__name__,
645 h(x, y), predVal, x, y, params, "clone")
646 self.assertFloatsAlmostEqual(
647 h(x, y), predVal, msg=msg, atol=2e-12, rtol=None)
649 # test that the number of parameters is correct for the given order
650 def numParamsFromOrder(order):
651 return (order + 1) * (order + 2) // 2
652 MaxOrder = 13
653 for order in range(MaxOrder+1):
654 f = afwMath.PolynomialFunction2D(order)
655 predNParams = numParamsFromOrder(order)
656 self.assertEqual(f.getNParameters(), predNParams)
657 afwMath.PolynomialFunction2D(np.zeros(predNParams, dtype=float))
659 # test that the wrong number of parameters raises an exception
660 validNumParams = set()
661 for order in range(MaxOrder+1):
662 validNumParams.add(numParamsFromOrder(order))
663 for numParams in range(numParamsFromOrder(MaxOrder)):
664 if numParams in validNumParams:
665 continue
666 with self.assertRaises(pexExceptions.InvalidParameterError):
667 afwMath.PolynomialFunction2D(np.zeros(numParams, dtype=float))
669 # test that changing parameters clears the cache
670 order = 3
671 numParams = numParamsFromOrder(order)
672 f = afwMath.PolynomialFunction2D(order)
673 x = 0.5
674 y = -0.24
675 for addValue in (0.0, 0.2):
676 params = nrange(numParams, deltaParam + addValue, deltaParam)
677 f.setParameters(params)
678 predVal = basic2DPoly(x, y, params)
679 msg = errMsg.format(type(f).__name__, f(x, y),
680 predVal, x, y, params, "")
681 self.assertFloatsAlmostEqual(
682 f(x, y), predVal, msg=msg, atol=self.atol, rtol=None)
684 def testDFuncDParameters(self):
685 """Test that we can differentiate the Function2 with respect to its parameters"""
686 nOrder = 3
687 params = []
688 for i in range((nOrder + 1)*(nOrder + 2)//2):
689 # deterministic pretty-random numbers
690 params.append(math.sin(1 + i))
692 f = afwMath.PolynomialFunction2D(params)
694 for (x, y) in [(2, 1), (1, 2), (2, 2)]:
695 dFdC = f.getDFuncDParameters(x, y)
697 self.assertAlmostEqual(
698 f(x, y),
699 sum([params[i]*dFdC[i] for i in range(len(params))]))
702class MemoryTester(lsst.utils.tests.MemoryTestCase):
703 pass
706def setup_module(module):
707 lsst.utils.tests.init()
710if __name__ == "__main__": 710 ↛ 711line 710 didn't jump to line 711, because the condition on line 710 was never true
711 lsst.utils.tests.init()
712 unittest.main()