Coverage for tests/test_photometryTransform.py: 24%
222 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-21 09:26 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-21 09:26 +0000
1# This file is part of jointcal.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import abc
24import numpy as np
26import unittest
27import lsst.utils.tests
29import lsst.geom
30import lsst.jointcal
33CHEBYSHEV_T = [ 33 ↛ exitline 33 didn't jump to the function exit
34 lambda x: 1,
35 lambda x: x,
36 lambda x: 2*x**2 - 1,
37 lambda x: (4*x**2 - 3)*x,
38 lambda x: (8*x**2 - 8)*x**2 + 1,
39 lambda x: ((16*x**2 - 20)*x**2 + 5)*x,
40]
43class PhotometryTransformTestBase:
44 def setUp(self):
45 self.value = 5.0
46 self.valueError = 0.3
47 self.point = [1., 5.]
50class SpatiallyInvariantTestBase(PhotometryTransformTestBase):
51 """Tests for PhotometryTransformSpatiallyInvariant.
52 Subclasses need to call setUp to define:
53 self.transform1 == a default initalized PhotometryTransformSpatiallyInvariant.
54 self.transform2 == a transform initialized with self.t2InitValue.
55 """
56 def setUp(self):
57 super().setUp()
58 # initial values for self.transform2
59 self.t2InitValue = 1000.0
60 self.t2InitError = 70.0
62 def _test_transform(self, transform, expect):
63 result = transform.transform(self.point[0], self.point[1], self.value)
64 self.assertEqual(result, expect) # yes, I really mean exactly equal
66 def _test_transformError(self, transform, expect):
67 result = transform.transformError(self.point[0], self.point[1], self.value, self.valueError)
68 self.assertFloatsAlmostEqual(result, expect)
70 def _offsetParams(self, delta, value, expect):
71 self.transform1.offsetParams(delta)
72 result = self.transform1.transform(self.point[0], self.point[1], value)
73 self.assertFloatsAlmostEqual(result, expect)
75 def _test_offsetParams(self, expect):
76 """Test offsetting; note that offsetParams offsets by +1."""
77 # check that offset by 0 doesn't change anything.
78 delta = np.zeros(1, dtype=float)
79 self._offsetParams(delta, self.value, self.value)
81 # offset by +1 should result in `expect`
82 delta -= 1
83 self._offsetParams(delta, self.value, expect)
85 def test_clone(self):
86 clone1 = self.transform1.clone()
87 self.assertEqual(self.transform1.getParameters(), clone1.getParameters())
88 clone2 = self.transform2.clone()
89 self.assertEqual(self.transform2.getParameters(), clone2.getParameters())
90 self.assertNotEqual(clone1.getParameters(), clone2.getParameters())
92 def _test_computeParameterDerivatives(self, expect):
93 """The derivative of a spatially invariant transform is always the same.
94 Should be indepdendent of position
95 """
96 result = self.transform1.computeParameterDerivatives(1, 2, self.value)
97 self.assertEqual(expect, result)
98 result = self.transform1.computeParameterDerivatives(-5, -100, self.value)
99 self.assertEqual(expect, result)
100 result = self.transform2.computeParameterDerivatives(-1000, 150, self.value)
101 self.assertEqual(expect, result)
104class FluxTransformSpatiallyInvariantTestCase(SpatiallyInvariantTestBase, lsst.utils.tests.TestCase):
105 def setUp(self):
106 super().setUp()
107 self.transform1 = lsst.jointcal.FluxTransformSpatiallyInvariant()
108 self.transform2 = lsst.jointcal.FluxTransformSpatiallyInvariant(self.t2InitValue)
110 def test_transform(self):
111 self._test_transform(self.transform1, self.value)
112 self._test_transform(self.transform2, self.value*self.t2InitValue)
114 def test_transformError(self):
115 expect = (self.valueError*1)
116 self._test_transformError(self.transform1, expect)
117 expect = (self.valueError*self.t2InitValue)
118 self._test_transformError(self.transform2, expect)
120 def test_offsetParams(self):
121 """Offset by +1 means transform by 2."""
122 self._test_offsetParams(self.value*2)
124 def test_computeParameterDerivatives(self):
125 """Should be indepdendent of position, and equal to the flux."""
126 self._test_computeParameterDerivatives(self.value)
129class MagnitudeTransformSpatiallyInvariantTestCase(SpatiallyInvariantTestBase, lsst.utils.tests.TestCase):
130 def setUp(self):
131 super().setUp()
132 self.transform1 = lsst.jointcal.MagnitudeTransformSpatiallyInvariant()
133 self.transform2 = lsst.jointcal.MagnitudeTransformSpatiallyInvariant(self.t2InitValue)
135 def test_transform(self):
136 self._test_transform(self.transform1, self.value)
137 self._test_transform(self.transform2, self.value + self.t2InitValue)
139 def test_transformError(self):
140 expect = self.valueError
141 self._test_transformError(self.transform1, expect)
142 expect = self.valueError
143 self._test_transformError(self.transform2, expect)
145 def test_offsetParams(self):
146 """Offset by +1 means transform by +1."""
147 self._test_offsetParams(self.value + 1)
149 def test_computeParameterDerivatives(self):
150 """Should always be identically 1."""
151 self._test_computeParameterDerivatives(1.0)
154class PhotometryTransformChebyshevTestCase(PhotometryTransformTestBase, abc.ABC):
155 def setUp(self):
156 """Call this first, then construct self.transform1 from self.order1,
157 and self.transform2 from self.coefficients.
158 """
159 super().setUp()
160 self.bbox = lsst.geom.Box2D(lsst.geom.Point2D(-5, -6), lsst.geom.Point2D(7, 8))
161 self.order1 = 2
162 self.coefficients = np.array([[5, 3], [4, 0]], dtype=float)
164 # self.transform1 will have 6 parameters, by construction
165 self.delta = np.arange(6, dtype=float)
166 # make one of them have opposite sign to check +/- consistency
167 self.delta[0] = -self.delta[0]
169 def test_getNpar(self):
170 self.assertEqual(self.transform1.getNpar(), 6)
171 self.assertEqual(self.transform2.getNpar(), 3)
173 def _evaluate_chebyshev(self, x, y):
174 """Evaluate the chebyshev defined by self.coefficients at (x,y)"""
175 # sx, sy: transform from self.bbox range to [-1, -1]
176 cx = (self.bbox.getMinX() + self.bbox.getMaxX())/2.0
177 cy = (self.bbox.getMinY() + self.bbox.getMaxY())/2.0
178 sx = 2.0 / self.bbox.getWidth()
179 sy = 2.0 / self.bbox.getHeight()
180 result = 0
181 order = len(self.coefficients)
182 for j in range(order):
183 for i in range(0, order-j):
184 Tx = CHEBYSHEV_T[i](sx*(x - cx))
185 Ty = CHEBYSHEV_T[j](sy*(y - cy))
186 result += self.coefficients[j, i]*Tx*Ty
187 return result
189 def _test_offsetParams(self, expect):
190 """Test offsetting; note that offsetParams offsets by `-delta`.
192 Parameters
193 ----------
194 expect1 : `numpy.ndarray`, (N,2)
195 Expected coefficients from an offset by 0.
196 expect2 : `numpy.ndarray`, (N,2)
197 Expected coefficients from an offset by self.delta.
198 """
199 # first offset by all zeros: nothing should change
200 delta = np.zeros(self.transform1.getNpar(), dtype=float)
201 self.transform1.offsetParams(delta)
202 self.assertFloatsAlmostEqual(expect, self.transform1.getCoefficients())
204 # now offset by self.delta
205 expect[0, 0] -= self.delta[0]
206 expect[0, 1] -= self.delta[1]
207 expect[0, 2] -= self.delta[2]
208 expect[1, 0] -= self.delta[3]
209 expect[1, 1] -= self.delta[4]
210 expect[2, 0] -= self.delta[5]
211 self.transform1.offsetParams(self.delta)
212 self.assertFloatsAlmostEqual(expect, self.transform1.getCoefficients())
214 def test_clone(self):
215 clone1 = self.transform1.clone()
216 self.assertFloatsEqual(self.transform1.getParameters(), clone1.getParameters())
217 self.assertEqual(self.transform1.getOrder(), clone1.getOrder())
218 self.assertEqual(self.transform1.getBBox(), clone1.getBBox())
219 clone2 = self.transform2.clone()
220 self.assertFloatsEqual(self.transform2.getParameters(), clone2.getParameters())
221 self.assertEqual(self.transform2.getOrder(), clone2.getOrder())
222 self.assertEqual(self.transform2.getBBox(), clone2.getBBox())
224 @abc.abstractmethod
225 def _computeChebyshevDerivative(self, Tx, Ty, value):
226 """Return the derivative of chebyshev component Tx, Ty."""
227 pass
229 def test_computeParameterDerivatives(self):
230 cx = (self.bbox.getMinX() + self.bbox.getMaxX())/2.0
231 cy = (self.bbox.getMinY() + self.bbox.getMaxY())/2.0
232 sx = 2.0 / self.bbox.getWidth()
233 sy = 2.0 / self.bbox.getHeight()
234 result = self.transform1.computeParameterDerivatives(self.point[0], self.point[1], self.value)
235 Tx = np.array([CHEBYSHEV_T[i](sx*(self.point[0] - cx)) for i in range(self.order1+1)], dtype=float)
236 Ty = np.array([CHEBYSHEV_T[i](sy*(self.point[1] - cy)) for i in range(self.order1+1)], dtype=float)
237 expect = []
238 for j in range(len(Ty)):
239 for i in range(0, self.order1-j+1):
240 expect.append(self._computeChebyshevDerivative(Ty[j], Tx[i], self.value))
241 self.assertFloatsAlmostEqual(np.array(expect), result)
243 def testIntegrateBoxOrder0(self):
244 r"""Test integrating over an "interesting" box.
246 The values of these integrals were checked in Mathematica. The code
247 block below can be pasted into Mathematica to re-do those calculations.
249 .. code-block:: mathematica
251 f[x_, y_, n_, m_] := \!\(
252 \*UnderoverscriptBox[\(\[Sum]\), \(i = 0\), \(n\)]\(
253 \*UnderoverscriptBox[\(\[Sum]\), \(j = 0\), \(m\)]
254 \*SubscriptBox[\(a\), \(i, j\)]*ChebyshevT[i, x]*ChebyshevT[j, y]\)\)
255 integrate2dBox[n_, m_, xmin_, xmax_, ymin_, ymax_, x0_, x1_, y0_,
256 y1_] := \!\(
257 \*SubsuperscriptBox[\(\[Integral]\), \(y0\), \(y1\)]\(
258 \*SubsuperscriptBox[\(\[Integral]\), \(x0\), \(x1\)]f[
259 \*FractionBox[\(2 x - xmin - xmax\), \(xmax - xmin\)],
260 \*FractionBox[\(2 y - ymin - ymax\), \(ymax - ymin\)], n,
261 m] \[DifferentialD]x \[DifferentialD]y\)\)
262 integrate2dBox[0, 0, -5, 7, -6, 8, 0, 7, 0, 8]
263 integrate2dBox[0, 0, -5, 7, -6, 8, 2, 6, 3, 5]
264 integrate2dBox[1, 0, -5, 7, -6, 8, 0, 6, 0, 5]
265 integrate2dBox[0, 1, -5, 7, -6, 8, 0, 6, 0, 5]
266 integrate2dBox[1, 1, -5, 7, -6, 8, -1, 5., 2, 7]
267 integrate2dBox[2, 2, -5, 7, -6, 8, 0, 2, 0, 3]
268 """
269 coeffs = np.array([[3.]], dtype=float)
270 transform = lsst.jointcal.FluxTransformChebyshev(coeffs, self.bbox)
272 # a box that goes from 0,0 to the x/y maximum
273 box = lsst.geom.Box2D(lsst.geom.Point2D(0, 0),
274 lsst.geom.Point2D(self.bbox.getMaxX(), self.bbox.getMaxY()))
275 expect = 56*coeffs[0]
276 result = transform.integrate(box)
277 self.assertFloatsAlmostEqual(result, expect)
279 # Different box
280 box = lsst.geom.Box2D(lsst.geom.Point2D(2, 3), lsst.geom.Point2D(6, 5))
281 expect = 8*coeffs[0]
282 result = transform.integrate(box)
283 self.assertFloatsAlmostEqual(result, expect)
285 def testIntegrateBoxOrder1(self):
286 """Test integrating 1st order in x or y.
287 Note that the coefficients are [y,x] ordered.
288 """
289 box = lsst.geom.Box2D(lsst.geom.Point2D(0, 0), lsst.geom.Point2D(6, 5))
290 # test 1st order in x:
291 coeffs = np.array([[2., 5.], [0., 0]], dtype=float)
292 transform = lsst.jointcal.FluxTransformChebyshev(coeffs, self.bbox)
293 # 30*a00 + 10*a10
294 expect = 30*coeffs[0, 0] + 10*coeffs[0, 1]
295 result = transform.integrate(box)
296 self.assertFloatsAlmostEqual(result, expect)
298 # test 1st order in y:
299 coeffs = np.array([[2., 0.], [5., 0]], dtype=float)
300 transform = lsst.jointcal.FluxTransformChebyshev(coeffs, self.bbox)
301 # 30*a00 + 45/7*a01
302 expect = 30*coeffs[0, 0] + 45./7.*coeffs[1, 0]
303 result = transform.integrate(box)
304 self.assertFloatsAlmostEqual(result, expect)
306 def testIntegrateBoxOrder2(self):
307 """Test integrating 1st order in both x and y.
308 Note that the coefficients are [y,x] ordered.
309 """
310 # 1st order in both x and y
311 transform = lsst.jointcal.FluxTransformChebyshev(2, self.bbox)
312 # zero, then set the parameters
313 transform.offsetParams(np.array([1, 0, 0, 0, 0, 0, 0, 0, 0], dtype=float))
314 coeffs = np.array([[0, 0, 0], [0, 4, 0], [0, 0, 0]], dtype=float)
315 transform.offsetParams(-coeffs.flatten())
317 # integrate on the smaller box:
318 box = lsst.geom.Box2D(lsst.geom.Point2D(-1, 2), lsst.geom.Point2D(5, 7))
319 # 5/2*(12*a0,0 + 6*a0,1 + 2*a1,0 + a1,1)
320 expect = 5/2 * (12*coeffs[0, 0] + 6*coeffs[1, 0] + 2*coeffs[0, 1] + coeffs[1, 1])
322 result = transform.integrate(box)
323 self.assertFloatsAlmostEqual(result, expect)
325 def testIntegrateBoxOrder4(self):
326 """Test integrating 2nd order in both x and y.
327 Note that the coefficients are [y,x] ordered.
328 """
329 # for 2nd order in both x and y
330 box = lsst.geom.Box2D(lsst.geom.Point2D(-3, 0), lsst.geom.Point2D(2, 3))
331 coeffs = np.array([[1, 2, 3, 0, 0], [4, 5, 6, 0, 0], [7, 8, 9, 0, 0],
332 [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], dtype=float)
333 transform = lsst.jointcal.FluxTransformChebyshev(coeffs, self.bbox)
335 # integrating on the full box should match the standard integral
336 expect = transform.integrate()
337 result = transform.integrate(self.bbox)
338 self.assertFloatsAlmostEqual(result, expect, rtol=6e-16)
340 # 5/3528 * (10584*a00 + 756*a01 - 10152*a02 - 2646*a10 - 189*a11 +
341 # 2538*a12 - 8036*a20 - 574*a21 + 7708*a22)
342 expect = 5/3528 * (10584*coeffs[0, 0] + 756*coeffs[1, 0]
343 - 10152*coeffs[2, 0] - 2646*coeffs[0, 1]
344 - 189*coeffs[1, 1] + 2538*coeffs[2, 1]
345 - 8036*coeffs[0, 2] - 574*coeffs[1, 2]
346 + 7708*coeffs[2, 2])
347 result = transform.integrate(box)
348 self.assertFloatsAlmostEqual(result, expect, rtol=2e-14)
351class FluxTransformChebyshevTestCase(PhotometryTransformChebyshevTestCase, lsst.utils.tests.TestCase):
352 def setUp(self):
353 super().setUp()
354 self.transform1 = lsst.jointcal.FluxTransformChebyshev(self.order1, self.bbox)
355 self.transform2 = lsst.jointcal.FluxTransformChebyshev(self.coefficients, self.bbox)
357 def test_transform(self):
358 result = self.transform1.transform(self.point[0], self.point[1], self.value)
359 self.assertEqual(result, self.value) # transform1 is the identity
361 result = self.transform2.transform(self.point[0], self.point[1], self.value)
362 expect = self.value*self._evaluate_chebyshev(self.point[0], self.point[1])
363 self.assertEqual(result, expect)
365 def test_offsetParams(self):
366 # an offset by 0 means we will still have 1 only in the 0th parameter
367 expect = np.zeros((self.order1+1, self.order1+1), dtype=float)
368 expect[0, 0] = 1
369 self._test_offsetParams(expect)
371 def _computeChebyshevDerivative(self, x, y, value):
372 return x * y * value
375class MagnitudeTransformChebyshevTestCase(PhotometryTransformChebyshevTestCase, lsst.utils.tests.TestCase):
376 def setUp(self):
377 super().setUp()
378 self.transform1 = lsst.jointcal.MagnitudeTransformChebyshev(self.order1, self.bbox)
379 self.transform2 = lsst.jointcal.MagnitudeTransformChebyshev(self.coefficients, self.bbox)
381 def test_transform(self):
382 result = self.transform1.transform(self.point[0], self.point[1], self.value)
383 self.assertEqual(result, self.value) # transform1 is the identity
385 result = self.transform2.transform(self.point[0], self.point[1], self.value)
386 expect = self.value + self._evaluate_chebyshev(self.point[0], self.point[1])
387 self.assertEqual(result, expect)
389 def test_offsetParams(self):
390 # an offset by 0 means all parameters still 0
391 expect = np.zeros((self.order1+1, self.order1+1), dtype=float)
392 self._test_offsetParams(expect)
394 def _computeChebyshevDerivative(self, x, y, value):
395 return x * y
398class MemoryTester(lsst.utils.tests.MemoryTestCase):
399 pass
402def setup_module(module):
403 lsst.utils.tests.init()
406if __name__ == "__main__": 406 ↛ 407line 406 didn't jump to line 407, because the condition on line 406 was never true
407 lsst.utils.tests.init()
408 unittest.main()