Coverage for tests/test_transformFactory.py: 15%
227 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-22 03:22 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-22 03:22 -0800
1#
2# LSST Data Management System
3# Copyright 2017 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#
23"""Tests for custom Transforms and their factories
24"""
26import math
27import unittest
29import numpy as np
30from numpy.testing import assert_allclose
32from astshim.test import makeForwardPolyMap
34import lsst.geom
35import lsst.afw.geom as afwGeom
36from lsst.afw.geom.testUtils import TransformTestBaseClass
37import lsst.pex.exceptions as pexExcept
38import lsst.utils.tests
41class TransformFactoryTestSuite(TransformTestBaseClass):
43 def setUp(self):
44 TransformTestBaseClass.setUp(self)
45 self.endpointPrefixes = tuple(
46 x for x in self.endpointPrefixes if x != "SpherePoint")
48 def point2DList(self):
49 for x in (-1.1, 0, 2.2):
50 for y in (3.1, 0, 2.1):
51 yield lsst.geom.Point2D(x, y)
53 def testLinearize(self):
54 for transform, invertible in (
55 (afwGeom.TransformPoint2ToPoint2(makeForwardPolyMap(2, 2)), False),
56 (afwGeom.makeIdentityTransform(), True),
57 (afwGeom.makeTransform(lsst.geom.AffineTransform(np.array([[3.0, -2.0], [2.0, -1.0]]))), True),
58 (afwGeom.makeRadialTransform([0.0, 8.0e-05, 0.0, -4.5e-12]), True),
59 ):
60 self.checkLinearize(transform, invertible)
62 def checkLinearize(self, transform, invertible):
63 """Test whether a specific transform is correctly linearized.
65 Parameters
66 ----------
67 transform: `lsst.afw.geom.Transform`
68 the transform whose linearization will be tested. Should not be
69 strongly curved within ~1 unit of the origin, or the test may rule
70 the approximation isn't good enough.
71 invertible: `bool`
72 whether `transform` is invertible. The test will verify that the
73 linearized form is invertible iff `transform` is. If `transform`
74 is invertible, the test will also verify that the inverse of the
75 linearization approximates the inverse of `transform`.
76 """
77 fromEndpoint = transform.fromEndpoint
78 toEndpoint = transform.toEndpoint
79 nIn = fromEndpoint.nAxes
80 nOut = toEndpoint.nAxes
81 msg = "TransformClass={}, nIn={}, nOut={}".format(type(transform).__name__, nIn, nOut)
83 rawLinPoint = self.makeRawPointData(nIn)
84 linPoint = fromEndpoint.pointFromData(rawLinPoint)
85 affine = afwGeom.linearizeTransform(transform, linPoint)
86 self.assertIsInstance(affine, lsst.geom.AffineTransform)
88 # Does affine match exact transform at linPoint?
89 outPoint = transform.applyForward(linPoint)
90 outPointLinearized = affine(linPoint)
91 assert_allclose(toEndpoint.dataFromPoint(outPoint),
92 toEndpoint.dataFromPoint(outPointLinearized),
93 err_msg=msg)
94 jacobian = transform.getJacobian(linPoint)
95 jacobianLinearized = affine.getLinear().getMatrix()
96 assert_allclose(jacobian, jacobianLinearized)
98 # Is affine a local approximation around linPoint?
99 for deltaFrom in (
100 np.zeros(nIn),
101 np.full(nIn, 0.1),
102 np.array([0.1, -0.15, 0.20, -0.05, 0.0, -0.1][0:nIn])
103 ):
104 tweakedInPoint = fromEndpoint.pointFromData(
105 rawLinPoint + deltaFrom)
106 tweakedOutPoint = transform.applyForward(tweakedInPoint)
107 tweakedOutPointLinearized = affine(tweakedInPoint)
108 assert_allclose(
109 toEndpoint.dataFromPoint(tweakedOutPoint),
110 toEndpoint.dataFromPoint(tweakedOutPointLinearized),
111 atol=1e-3,
112 err_msg=msg)
114 # Is affine invertible?
115 # AST lets all-zero MatrixMaps be invertible though inverse
116 # ill-defined; exclude this case
117 if invertible:
118 rng = np.random.RandomState(42)
119 nDelta = 100
120 inverse = affine.inverted()
121 deltaFrom = rng.normal(0.0, 10.0, (nIn, nDelta))
122 for i in range(nDelta):
123 pointMsg = "{}, point={}".format(msg, tweakedInPoint)
124 tweakedInPoint = fromEndpoint.pointFromData(
125 rawLinPoint + deltaFrom[:, i])
126 tweakedOutPoint = affine(tweakedInPoint)
128 roundTrip = inverse(tweakedOutPoint)
129 assert_allclose(
130 roundTrip, tweakedInPoint,
131 err_msg=pointMsg)
132 assert_allclose(
133 inverse.getLinear().getMatrix(),
134 np.linalg.inv(jacobian),
135 err_msg=pointMsg)
136 else:
137 # TODO: replace with correct type after fixing DM-11248
138 with self.assertRaises(Exception):
139 affine.inverted()
141 # Can't test exceptions without reliable way to make invalid transform
143 def checkGenericTransform(self, tFactory, tConfig, transformCheck=None,
144 **kwargs):
145 """Check Transform by building it from a factory.
146 """
147 self.checkConfig(tFactory, tConfig, transformCheck, **kwargs)
149 with lsst.utils.tests.getTempFilePath(".py") as filePath:
150 tConfig.save(filePath)
151 loadConfig = tConfig.__class__()
152 loadConfig.load(filePath)
153 self.checkConfig(tFactory, loadConfig, transformCheck, **kwargs)
155 def checkConfig(self, tFactory, tConfig, transformCheck, **kwargs):
156 """Check Transform built from a particular config
157 """
158 transform = tFactory(tConfig)
159 self.checkRoundTrip(transform, **kwargs)
160 if transformCheck is not None:
161 transformCheck(transform)
163 def checkRoundTrip(self, transform, **kwargs):
164 """Check round trip of transform
165 """
166 for fromPoint in self.point2DList():
167 toPoint = transform.applyForward(fromPoint)
168 roundTripPoint = transform.applyInverse(toPoint)
169 # Don't let NaNs pass the test!
170 assert_allclose(roundTripPoint, fromPoint, atol=1e-14, **kwargs)
172 def testIdentity(self):
173 """Test identity transform.
174 """
175 identFactory = afwGeom.transformRegistry["identity"]
176 identConfig = identFactory.ConfigClass()
177 self.checkGenericTransform(identFactory, identConfig,
178 self.checkIdentity)
180 def checkIdentity(self, transform):
181 for fromPoint in self.point2DList():
182 toPoint = transform.applyForward(fromPoint)
183 self.assertPairsAlmostEqual(fromPoint, toPoint)
185 def testDefaultAffine(self):
186 """Test affine = affine Transform with default coeffs (identity transform)
187 """
188 affineFactory = afwGeom.transformRegistry["affine"]
189 affineConfig = affineFactory.ConfigClass()
190 self.checkGenericTransform(affineFactory, affineConfig,
191 self.checkIdentity)
193 def testTranslateAffine(self):
194 """Test affine = affine Transform with just translation coefficients
195 """
196 affineFactory = afwGeom.transformRegistry["affine"]
197 affineConfig = affineFactory.ConfigClass()
198 affineConfig.translation = (1.2, -3.4)
200 def check(transform):
201 self.checkTranslateAffine(
202 transform,
203 lsst.geom.Extent2D(*affineConfig.translation))
204 self.checkGenericTransform(affineFactory, affineConfig, check)
206 def checkTranslateAffine(self, transform, offset):
207 for fromPoint in self.point2DList():
208 toPoint = transform.applyForward(fromPoint)
209 predToPoint = fromPoint + offset
210 self.assertPairsAlmostEqual(toPoint, predToPoint)
212 def testLinearAffine(self):
213 """Test affine = affine Transform with just linear coefficients
214 """
215 affineFactory = afwGeom.transformRegistry["affine"]
216 affineConfig = affineFactory.ConfigClass()
217 rotAng = 0.25 # radians
218 xScale = 1.2
219 yScale = 0.8
220 affineConfig.linear = (
221 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale,
222 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale,
223 )
225 def check(transform):
226 self.checkLinearAffine(transform, affineConfig.linear)
227 self.checkGenericTransform(affineFactory, affineConfig, check)
229 def checkLinearAffine(self, transform, matrix):
230 for fromPoint in self.point2DList():
231 toPoint = transform.applyForward(fromPoint)
232 predToPoint = lsst.geom.Point2D(
233 matrix[0] * fromPoint[0]
234 + matrix[1] * fromPoint[1],
235 matrix[2] * fromPoint[0]
236 + matrix[3] * fromPoint[1],
237 )
238 self.assertPairsAlmostEqual(toPoint, predToPoint)
240 def testFullAffine(self):
241 """Test affine = affine Transform with arbitrary coefficients
242 """
243 affineFactory = afwGeom.transformRegistry["affine"]
244 affineConfig = affineFactory.ConfigClass()
245 affineConfig.translation = (-2.1, 3.4)
246 rotAng = 0.832 # radians
247 xScale = 3.7
248 yScale = 45.3
249 affineConfig.linear = (
250 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale,
251 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale,
252 )
254 def check(transform):
255 self.checkFullAffine(
256 transform,
257 lsst.geom.Extent2D(*affineConfig.translation),
258 affineConfig.linear)
259 self.checkGenericTransform(affineFactory, affineConfig, check)
261 def checkFullAffine(self, transform, offset, matrix):
262 for fromPoint in self.point2DList():
263 toPoint = transform.applyForward(fromPoint)
264 predToPoint = lsst.geom.Point2D(
265 matrix[0] * fromPoint[0]
266 + matrix[1] * fromPoint[1],
267 matrix[2] * fromPoint[0]
268 + matrix[3] * fromPoint[1],
269 )
270 predToPoint = predToPoint + offset
271 self.assertPairsAlmostEqual(toPoint, predToPoint)
273 def testRadial(self):
274 """Test radial = radial Transform
275 """
276 radialFactory = afwGeom.transformRegistry["radial"]
277 radialConfig = radialFactory.ConfigClass()
278 radialConfig.coeffs = (0.0, 8.5165e-05, 0.0, -4.5014e-12)
280 def check(transform):
281 self.checkRadial(transform, radialConfig.coeffs)
282 self.checkGenericTransform(radialFactory, radialConfig, check)
284 invertibleCoeffs = (0.0, 1.0, 0.05)
285 inverseCoeffs = (0.0, 1.0, -0.05, 0.005, -0.000625, 0.0000875,
286 -1.3125e-5, 2.0625e-6, -3.3515625e-7, 5.5859375e-8,
287 -9.49609375e-9, 1.640234375e-9, -2.870410156e-10)
288 transform = afwGeom.makeRadialTransform(invertibleCoeffs,
289 inverseCoeffs)
290 self.checkRadialInvertible(transform, invertibleCoeffs)
292 def checkRadial(self, transform, coeffs):
293 if len(coeffs) < 4:
294 coeffs = tuple(coeffs + (0.0,) * (4 - len(coeffs)))
295 for fromPoint in self.point2DList():
296 fromRadius = math.hypot(fromPoint[0], fromPoint[1])
297 fromAngle = math.atan2(fromPoint[1], fromPoint[0])
298 predToRadius = fromRadius * \
299 (coeffs[3] * fromRadius**2 + coeffs[2] * fromRadius + coeffs[1])
300 if predToRadius > 0:
301 predToPoint = lsst.geom.Point2D(
302 predToRadius * math.cos(fromAngle),
303 predToRadius * math.sin(fromAngle))
304 else:
305 predToPoint = lsst.geom.Point2D()
306 toPoint = transform.applyForward(fromPoint)
307 # Don't let NaNs pass the test!
308 assert_allclose(toPoint, predToPoint, atol=1e-14)
310 def checkRadialInvertible(self, transform, coeffs):
311 self.checkRadial(transform, coeffs)
312 self.checkRoundTrip(transform, rtol=0.01)
314 def testBadRadial(self):
315 """Test radial with invalid coefficients
316 """
317 for badCoeffs in (
318 (0.0,), # len(coeffs) must be > 1
319 (0.1, 1.0), # coeffs[0] must be zero
320 (0.0, 0.0), # coeffs[1] must be nonzero
321 (0.0, 0.0, 0.1), # coeffs[1] must be nonzero
322 ):
323 with self.assertRaises(pexExcept.InvalidParameterError):
324 afwGeom.makeRadialTransform(badCoeffs)
326 radialFactory = afwGeom.transformRegistry["radial"]
327 radialConfig = radialFactory.ConfigClass()
328 radialConfig.coeffs = badCoeffs
329 with self.assertRaises(Exception):
330 radialConfig.validate()
332 def testInverted(self):
333 """Test radial = radial Transform
334 """
335 affineFactory = afwGeom.transformRegistry["affine"]
336 wrapper = afwGeom.OneTransformConfig()
337 wrapper.transform.retarget(affineFactory)
338 affineConfig = wrapper.transform
339 affineConfig.translation = (-2.1, 3.4)
340 rotAng = 0.832 # radians
341 xScale = 3.7
342 yScale = 45.3
343 affineConfig.linear = (
344 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale,
345 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale,
346 )
348 inverseFactory = afwGeom.transformRegistry["inverted"]
349 inverseConfig = inverseFactory.ConfigClass()
350 inverseConfig.transform = affineConfig
352 def check(transform):
353 self.checkInverted(transform, affineConfig.apply())
354 self.checkGenericTransform(inverseFactory, inverseConfig, check)
356 def checkInverted(self, transform, original):
357 for fromPoint in self.point2DList():
358 toPoint = transform.applyForward(fromPoint)
359 predToPoint = original.applyInverse(fromPoint)
360 self.assertPairsAlmostEqual(toPoint, predToPoint)
361 roundTrip = transform.applyInverse(toPoint)
362 predRoundTrip = original.applyForward(toPoint)
363 self.assertPairsAlmostEqual(roundTrip, predRoundTrip)
365 def testMulti(self):
366 """Test multi transform
367 """
368 affineFactory = afwGeom.transformRegistry["affine"]
369 wrapper0 = afwGeom.OneTransformConfig()
370 wrapper0.transform.retarget(affineFactory)
371 affineConfig0 = wrapper0.transform
372 affineConfig0.translation = (-2.1, 3.4)
373 rotAng = 0.832 # radians
374 xScale = 3.7
375 yScale = 45.3
376 affineConfig0.linear = (
377 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale,
378 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale,
379 )
381 wrapper1 = afwGeom.OneTransformConfig()
382 wrapper1.transform.retarget(affineFactory)
383 affineConfig1 = wrapper1.transform
384 affineConfig1.translation = (26.5, -35.1)
385 rotAng = -0.25 # radians
386 xScale = 1.45
387 yScale = 0.9
388 affineConfig1.linear = (
389 math.cos(rotAng) * xScale, math.sin(rotAng) * yScale,
390 -math.sin(rotAng) * xScale, math.cos(rotAng) * yScale,
391 )
393 multiFactory = afwGeom.transformRegistry["multi"]
394 multiConfig = multiFactory.ConfigClass()
395 multiConfig.transformDict = {
396 0: wrapper0,
397 1: wrapper1,
398 }
400 def check(transform):
401 self.checkMulti(transform,
402 [c.apply() for c in
403 [affineConfig0, affineConfig1]])
404 self.checkGenericTransform(multiFactory, multiConfig, check)
406 def checkMulti(self, multiTransform, transformList):
407 for fromPoint in self.point2DList():
408 toPoint = multiTransform.applyForward(fromPoint)
409 predToPoint = fromPoint
410 for transform in transformList:
411 predToPoint = transform.applyForward(predToPoint)
412 self.assertPairsAlmostEqual(toPoint, predToPoint)
415class MemoryTester(lsst.utils.tests.MemoryTestCase):
416 pass
419def setup_module(module):
420 lsst.utils.tests.init()
423if __name__ == "__main__": 423 ↛ 424line 423 didn't jump to line 424, because the condition on line 423 was never true
424 lsst.utils.tests.init()
425 unittest.main()