Coverage for tests/test_transforms.py: 16%
391 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-04 10:07 +0000
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-04 10:07 +0000
1#
2# LSST Data Management System
3# Copyright 2016-2017 LSST/AURA.
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 os
24import unittest
26import numpy as np
28import lsst.utils.tests
29import lsst.pex.exceptions
30import lsst.geom
31import lsst.afw.geom
32import lsst.afw.image
33import lsst.afw.math
34from lsst.afw.fits import readMetadata
35from lsst.afw.geom.testUtils import makeSipIwcToPixel, makeSipPixelToIwc
36from lsst.meas.astrom import (
37 PolynomialTransform,
38 ScaledPolynomialTransform,
39 SipForwardTransform,
40 SipReverseTransform,
41 ScaledPolynomialTransformFitter,
42 transformWcsPixels,
43 rotateWcsPixelsBy90
44)
45from lsst.afw.geom.wcsUtils import getSipMatrixFromMetadata, getCdMatrixFromMetadata
48def makeRandomCoefficientMatrix(n):
49 matrix = np.random.randn(n, n)
50 for i in range(1, n):
51 matrix[i, (n-i):] = 0
52 return matrix
55def makeRandomAffineTransform():
56 return lsst.geom.AffineTransform(
57 lsst.geom.LinearTransform(np.random.randn(2, 2)),
58 lsst.geom.Extent2D(*np.random.randn(2))
59 )
62def makeRandomPolynomialTransform(order, sip=False):
63 xc = makeRandomCoefficientMatrix(order + 1)
64 yc = makeRandomCoefficientMatrix(order + 1)
65 if sip:
66 xc[0, 0] = 0
67 yc[0, 0] = 0
68 xc[0, 1] = 0
69 yc[0, 1] = 0
70 xc[1, 0] = 0
71 yc[1, 0] = 0
72 return PolynomialTransform(xc, yc)
75def makeRandomScaledPolynomialTransform(order):
76 return ScaledPolynomialTransform(
77 makeRandomPolynomialTransform(order),
78 makeRandomAffineTransform(),
79 makeRandomAffineTransform()
80 )
83def makeRandomSipForwardTransform(order):
84 return SipForwardTransform(
85 lsst.geom.Point2D(*np.random.randn(2)),
86 lsst.geom.LinearTransform(np.random.randn(2, 2)),
87 makeRandomPolynomialTransform(order, sip=True)
88 )
91def makeRandomSipReverseTransform(order):
92 origin = lsst.geom.Point2D(*np.random.randn(2))
93 cd = lsst.geom.LinearTransform(np.random.randn(2, 2))
94 poly = makeRandomPolynomialTransform(order, sip=False)
95 return SipReverseTransform(origin, cd, poly)
98class TransformTestMixin:
100 def makeRandom(self):
101 """Create an instance of the transform being tested with random testing.
102 """
103 raise NotImplementedError()
105 def assertTransformsAlmostEqual(self, a, b, minval=0.0, maxval=1.0, atol=0, rtol=1E-8):
106 rangeval = maxval - minval
107 aArr = []
108 bArr = []
109 for i in range(10):
110 xy = rangeval * np.random.rand(2) - minval
111 point = lsst.geom.Point2D(*xy)
112 aArr.append(list(a(point)))
113 bArr.append(list(b(point)))
114 self.assertFloatsAlmostEqual(np.array(aArr), np.array(bArr), atol=atol, rtol=rtol)
116 def testLinearize(self):
117 """Test that the AffineTransform returned by linearize() is equivalent
118 to the transform at the expansion point, and matches finite differences.
119 """
120 transform = self.makeRandom()
121 point = lsst.geom.Point2D(*np.random.randn(2))
122 affine = transform.linearize(point)
123 self.assertFloatsAlmostEqual(np.array(transform(point)), np.array(affine(point)), rtol=1E-14)
124 delta = 1E-4
125 deltaX = lsst.geom.Extent2D(delta, 0.0)
126 deltaY = lsst.geom.Extent2D(0.0, delta)
127 dtdx = (transform(point + deltaX) - transform(point - deltaX)) / (2*delta)
128 dtdy = (transform(point + deltaY) - transform(point - deltaY)) / (2*delta)
129 self.assertFloatsAlmostEqual(affine[affine.XX], dtdx.getX(), rtol=1E-6)
130 self.assertFloatsAlmostEqual(affine[affine.YX], dtdx.getY(), rtol=1E-6)
131 self.assertFloatsAlmostEqual(affine[affine.XY], dtdy.getX(), rtol=1E-6)
132 self.assertFloatsAlmostEqual(affine[affine.YY], dtdy.getY(), rtol=1E-6)
135class PolynomialTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin):
137 def setUp(self):
138 np.random.seed(50)
140 def makeRandom(self):
141 return makeRandomPolynomialTransform(4)
143 def testArrayConstructor(self):
144 """Test that construction with coefficient arrays yields an object with
145 copies of those arrays, and that all dimensions must be the same.
146 """
147 order = 3
148 xc = makeRandomCoefficientMatrix(order + 1)
149 yc = makeRandomCoefficientMatrix(order + 1)
150 p = PolynomialTransform(xc, yc)
151 self.assertEqual(p.getOrder(), order)
152 self.assertFloatsAlmostEqual(p.getXCoeffs(), xc, atol=0, rtol=0)
153 self.assertFloatsAlmostEqual(p.getYCoeffs(), yc, atol=0, rtol=0)
154 # Test that the coefficients are not a view.
155 old = xc[0, 0]
156 xc[0, 0] += 100.0
157 self.assertEqual(p.getXCoeffs()[0, 0], old)
158 # Test that rectangular coefficient arrays are not allowed.
159 self.assertRaises(
160 lsst.pex.exceptions.LengthError,
161 PolynomialTransform,
162 np.zeros((5, 4), dtype=float),
163 np.zeros((5, 4), dtype=float)
164 )
165 # Test that x and y coefficient arrays must have the same shape.
166 self.assertRaises(
167 lsst.pex.exceptions.LengthError,
168 PolynomialTransform,
169 np.zeros((5, 5), dtype=float),
170 np.zeros((4, 4), dtype=float)
171 )
173 def testConvertScaledPolynomial(self):
174 """Test that we can convert a ScaledPolynomialTransform to a PolynomialTransform.
175 """
176 scaled = makeRandomScaledPolynomialTransform(4)
177 converted = PolynomialTransform.convert(scaled)
178 self.assertTransformsAlmostEqual(scaled, converted)
180 def testConvertSipForward(self):
181 """Test that we can convert a SipForwardTransform to a PolynomialTransform.
182 """
183 sipForward = makeRandomSipForwardTransform(4)
184 converted = PolynomialTransform.convert(sipForward)
185 self.assertTransformsAlmostEqual(sipForward, converted)
187 def testConvertSipReverse(self):
188 """Test that we can convert a SipForwardTransform to a PolynomialTransform.
189 """
190 sipReverse = makeRandomSipReverseTransform(4)
191 converted = PolynomialTransform.convert(sipReverse)
192 self.assertTransformsAlmostEqual(sipReverse, converted)
194 def testCompose(self):
195 """Test that AffineTransforms and PolynomialTransforms can be composed
196 into an equivalent PolynomialTransform.
197 """
198 poly = makeRandomPolynomialTransform(4)
199 affine = lsst.geom.AffineTransform(
200 lsst.geom.LinearTransform(np.random.randn(2, 2)),
201 lsst.geom.Extent2D(*np.random.randn(2))
202 )
203 composed1 = lsst.meas.astrom.compose(poly, affine)
204 composed2 = lsst.meas.astrom.compose(affine, poly)
205 self.assertTransformsAlmostEqual(composed1, lambda p: poly(affine(p)))
206 self.assertTransformsAlmostEqual(composed2, lambda p: affine(poly(p)))
207 # Test that composition with an identity transform is a no-op
208 composed3 = lsst.meas.astrom.compose(poly, lsst.geom.AffineTransform())
209 composed4 = lsst.meas.astrom.compose(lsst.geom.AffineTransform(), poly)
210 self.assertFloatsAlmostEqual(composed3.getXCoeffs(), poly.getXCoeffs())
211 self.assertFloatsAlmostEqual(composed3.getYCoeffs(), poly.getYCoeffs())
212 self.assertFloatsAlmostEqual(composed4.getXCoeffs(), poly.getXCoeffs())
213 self.assertFloatsAlmostEqual(composed4.getYCoeffs(), poly.getYCoeffs())
216class ScaledPolynomialTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin):
218 def setUp(self):
219 np.random.seed(50)
221 def makeRandom(self):
222 return makeRandomScaledPolynomialTransform(4)
224 def testConstruction(self):
225 poly = makeRandomPolynomialTransform(4)
226 inputScaling = makeRandomAffineTransform()
227 outputScalingInverse = makeRandomAffineTransform()
228 scaled = ScaledPolynomialTransform(poly, inputScaling, outputScalingInverse)
229 self.assertTransformsAlmostEqual(
230 scaled,
231 lambda p: outputScalingInverse(poly(inputScaling(p)))
232 )
234 def testConvertPolynomial(self):
235 """Test that we can convert a PolynomialTransform to a ScaledPolynomialTransform.
236 """
237 poly = makeRandomPolynomialTransform(4)
238 converted = ScaledPolynomialTransform.convert(poly)
239 self.assertTransformsAlmostEqual(poly, converted)
241 def testConvertSipForward(self):
242 """Test that we can convert a SipForwardTransform to a ScaledPolynomialTransform.
243 """
244 sipForward = makeRandomSipForwardTransform(4)
245 converted = ScaledPolynomialTransform.convert(sipForward)
246 self.assertTransformsAlmostEqual(sipForward, converted)
248 def testConvertSipReverse(self):
249 """Test that we can convert a SipReverseTransform to a ScaledPolynomialTransform.
250 """
251 sipReverse = makeRandomSipReverseTransform(4)
252 converted = ScaledPolynomialTransform.convert(sipReverse)
253 self.assertTransformsAlmostEqual(sipReverse, converted)
256class SipForwardTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin):
258 def setUp(self):
259 np.random.seed(50)
261 def makeRandom(self):
262 return makeRandomSipForwardTransform(4)
264 def testConstruction(self):
265 poly = makeRandomPolynomialTransform(4, sip=True)
266 cd = lsst.geom.LinearTransform(np.random.randn(2, 2))
267 crpix = lsst.geom.Point2D(*np.random.randn(2))
268 sip = SipForwardTransform(crpix, cd, poly)
269 self.assertTransformsAlmostEqual(
270 sip,
271 lambda p: cd((p - crpix) + poly(lsst.geom.Point2D(p - crpix)))
272 )
274 def testConvertPolynomial(self):
275 poly = makeRandomPolynomialTransform(4)
276 cd = lsst.geom.LinearTransform(np.random.randn(2, 2))
277 crpix = lsst.geom.Point2D(*np.random.randn(2))
278 sip = lsst.meas.astrom.SipForwardTransform.convert(poly, crpix, cd)
279 self.assertTransformsAlmostEqual(sip, poly)
281 def testConvertScaledPolynomialManual(self):
282 scaled = makeRandomScaledPolynomialTransform(4)
283 cd = lsst.geom.LinearTransform(np.random.randn(2, 2))
284 crpix = lsst.geom.Point2D(*np.random.randn(2))
285 sip = lsst.meas.astrom.SipForwardTransform.convert(scaled, crpix, cd)
286 self.assertTransformsAlmostEqual(sip, scaled)
288 def testConvertScaledPolynomialAutomatic(self):
289 scaled = makeRandomScaledPolynomialTransform(4)
290 sip = lsst.meas.astrom.SipForwardTransform.convert(scaled)
291 self.assertTransformsAlmostEqual(sip, scaled)
293 def testTransformPixels(self):
294 sip = makeRandomSipForwardTransform(4)
295 affine = makeRandomAffineTransform()
296 self.assertTransformsAlmostEqual(
297 sip.transformPixels(affine),
298 lambda p: sip(affine.inverted()(p))
299 )
301 def testMakeWcs(self):
302 """Test SipForwardTransform, SipReverseTransform and makeWcs
303 """
304 filename = os.path.join(os.path.dirname(__file__),
305 'imgCharSources-v85501867-R01-S00.sipheader')
306 sipMetadata = readMetadata(filename)
307 # We're building an ICRS-based TAN-SIP using coefficients read from metadata
308 # so ignore the RADESYS in metadata (which is missing anyway, falling back to FK5)
309 sipMetadata.set("RADESYS", "ICRS")
310 crpix = lsst.geom.Point2D(
311 sipMetadata.getScalar("CRPIX1") - 1,
312 sipMetadata.getScalar("CRPIX2") - 1,
313 )
314 crval = lsst.geom.SpherePoint(
315 sipMetadata.getScalar("CRVAL1"),
316 sipMetadata.getScalar("CRVAL2"), lsst.geom.degrees,
317 )
318 cdLinearTransform = lsst.geom.LinearTransform(getCdMatrixFromMetadata(sipMetadata))
319 aArr = getSipMatrixFromMetadata(sipMetadata, "A")
320 bArr = getSipMatrixFromMetadata(sipMetadata, "B")
321 apArr = getSipMatrixFromMetadata(sipMetadata, "AP")
322 bpArr = getSipMatrixFromMetadata(sipMetadata, "BP")
323 abPoly = PolynomialTransform(aArr, bArr)
324 abRevPoly = PolynomialTransform(apArr, bpArr)
325 fwd = SipForwardTransform(crpix, cdLinearTransform, abPoly)
326 rev = SipReverseTransform(crpix, cdLinearTransform, abRevPoly)
327 wcsFromMakeWcs = lsst.meas.astrom.makeWcs(fwd, rev, crval)
328 wcsFromMetadata = lsst.afw.geom.makeSkyWcs(sipMetadata, strip=False)
330 # Check SipForwardTransform against a local implementation
331 localPixelToIwc = makeSipPixelToIwc(sipMetadata)
332 self.assertTransformsAlmostEqual(fwd, localPixelToIwc.applyForward, maxval=2000)
334 # Compare SipReverseTransform against a local implementation
335 # Use the forward direction first to get sensible inputs
336 localIwcToPixel = makeSipIwcToPixel(sipMetadata)
338 def fwdThenRev(p):
339 return rev(fwd(p))
341 def fwdThenLocalRev(p):
342 return localIwcToPixel.applyForward(fwd(p))
344 self.assertTransformsAlmostEqual(fwdThenRev, fwdThenLocalRev, maxval=2000)
346 # Check that SipReverseTransform is the inverse of SipForwardTransform;
347 # this is not perfect because the coefficients don't define a perfect inverse
348 def nullTransform(p):
349 return p
351 self.assertTransformsAlmostEqual(fwdThenRev, nullTransform, maxval=2000, atol=1e-3)
353 # Check SipForwardTransform against the one contained in wcsFromMakeWcs
354 # (Don't bother with the other direction because the WCS transform is iterative,
355 # so it doesn't tell us anything useful about SipReverseTransform
356 pixelToIwc = lsst.afw.geom.getPixelToIntermediateWorldCoords(wcsFromMetadata)
357 self.assertTransformsAlmostEqual(fwd, pixelToIwc.applyForward, maxval=2000)
359 # Check a WCS constructed from SipForwardTransform, SipReverseTransform
360 # against one constructed directly from the metadata
361 bbox = lsst.geom.Box2D(lsst.geom.Point2D(0, 0), lsst.geom.Extent2D(2000, 2000))
362 self.assertWcsAlmostEqualOverBBox(wcsFromMakeWcs, wcsFromMetadata, bbox)
364 def testTransformWcsPixels(self):
365 filename = os.path.join(os.path.dirname(__file__),
366 'imgCharSources-v85501867-R01-S00.sipheader')
367 wcs1 = lsst.afw.geom.makeSkyWcs(readMetadata(filename))
368 s = makeRandomAffineTransform()
369 wcs2 = transformWcsPixels(wcs1, s)
370 crvalDeg = wcs1.getSkyOrigin().getPosition(lsst.geom.degrees)
372 def t1a(p):
373 raDeg, decDeg = crvalDeg + lsst.geom.Extent2D(p)
374 sky = lsst.geom.SpherePoint(raDeg, decDeg, lsst.geom.degrees)
375 return s(wcs1.skyToPixel(sky))
377 def t2a(p):
378 raDeg, decDeg = crvalDeg + lsst.geom.Extent2D(p)
379 sky = lsst.geom.SpherePoint(raDeg, decDeg, lsst.geom.degrees)
380 return wcs2.skyToPixel(sky)
382 self.assertTransformsAlmostEqual(t1a, t2a)
384 def t1b(p):
385 sky = wcs1.pixelToSky(s.inverted()(p))
386 return sky.getPosition(lsst.geom.degrees)
388 def t2b(p):
389 sky = wcs2.pixelToSky(p)
390 return sky.getPosition(lsst.geom.degrees)
392 self.assertTransformsAlmostEqual(t1b, t2b)
394 def testRotateWcsPixelsBy90(self):
395 filename = os.path.join(os.path.dirname(__file__),
396 'imgCharSources-v85501867-R01-S00.sipheader')
397 wcs0 = lsst.afw.geom.makeSkyWcs(readMetadata(filename))
398 w, h = 11, 12
399 image0 = lsst.afw.image.ImageD(w, h)
400 x, y = np.meshgrid(np.arange(w), np.arange(h))
401 # Make a slowly-varying image of an asymmetric function
402 image0.getArray()[:, :] = (x/w)**2 + 0.5*(x/w)*(y/h) - 3.0*(y/h)**2
403 dimensions = image0.getBBox().getDimensions()
405 image1 = lsst.afw.math.rotateImageBy90(image0, 1)
406 wcs1 = rotateWcsPixelsBy90(wcs0, 1, dimensions)
407 image2 = lsst.afw.math.rotateImageBy90(image0, 2)
408 wcs2 = rotateWcsPixelsBy90(wcs0, 2, dimensions)
409 image3 = lsst.afw.math.rotateImageBy90(image0, 3)
410 wcs3 = rotateWcsPixelsBy90(wcs0, 3, dimensions)
412 bbox = image0.getBBox()
413 image0r = lsst.afw.image.ImageD(bbox)
414 image1r = lsst.afw.image.ImageD(bbox)
415 image2r = lsst.afw.image.ImageD(bbox)
416 image3r = lsst.afw.image.ImageD(bbox)
418 ctrl = lsst.afw.math.WarpingControl("nearest")
419 lsst.afw.math.warpImage(image0r, wcs0, image0, wcs0, ctrl)
420 lsst.afw.math.warpImage(image1r, wcs0, image1, wcs1, ctrl)
421 lsst.afw.math.warpImage(image2r, wcs0, image2, wcs2, ctrl)
422 lsst.afw.math.warpImage(image3r, wcs0, image3, wcs3, ctrl)
424 # warpImage doesn't seem to handle the first row and column,
425 # even with nearest-neighbor interpolation, so we have to
426 # ignore pixels it didn't know how to populate.
427 def compareFinite(ref, target):
428 finitPixels = np.isfinite(target.getArray())
429 self.assertGreater(finitPixels.sum(), 0.7*target.getArray().size)
430 self.assertFloatsAlmostEqual(
431 ref.getArray()[finitPixels],
432 target.getArray()[finitPixels],
433 rtol=1E-6
434 )
436 compareFinite(image0, image0r)
437 compareFinite(image0, image1r)
438 compareFinite(image0, image2r)
439 compareFinite(image0, image3r)
442class SipReverseTransformTestCase(lsst.utils.tests.TestCase, TransformTestMixin):
444 def setUp(self):
445 np.random.seed(50)
447 def makeRandom(self):
448 return makeRandomSipReverseTransform(4)
450 def testConstruction(self):
451 poly = makeRandomPolynomialTransform(4)
452 cd = lsst.geom.LinearTransform(np.random.randn(2, 2))
453 crpix = lsst.geom.Point2D(*np.random.randn(2))
454 sip = SipReverseTransform(crpix, cd, poly)
455 offset = lsst.geom.Extent2D(crpix)
456 cdInverse = cd.inverted()
457 self.assertTransformsAlmostEqual(
458 sip,
459 lambda p: offset + lsst.geom.Extent2D(cdInverse(p)) + poly(cdInverse(p))
460 )
462 def testConvertPolynomial(self):
463 poly = makeRandomPolynomialTransform(4)
464 cd = lsst.geom.LinearTransform(np.random.randn(2, 2))
465 crpix = lsst.geom.Point2D(*np.random.randn(2))
466 sip = lsst.meas.astrom.SipReverseTransform.convert(poly, crpix, cd)
467 self.assertTransformsAlmostEqual(sip, poly)
469 def testConvertScaledPolynomialManual(self):
470 scaled = makeRandomScaledPolynomialTransform(4)
471 cd = lsst.geom.LinearTransform(np.random.randn(2, 2))
472 crpix = lsst.geom.Point2D(*np.random.randn(2))
473 sip = lsst.meas.astrom.SipReverseTransform.convert(scaled, crpix, cd)
474 self.assertTransformsAlmostEqual(sip, scaled)
476 def testConvertScaledPolynomialAutomatic(self):
477 scaled = makeRandomScaledPolynomialTransform(4)
478 sip = lsst.meas.astrom.SipReverseTransform.convert(scaled)
479 self.assertTransformsAlmostEqual(sip, scaled)
481 def testTransformPixels(self):
482 sip = makeRandomSipReverseTransform(4)
483 affine = makeRandomAffineTransform()
484 self.assertTransformsAlmostEqual(
485 sip.transformPixels(affine),
486 lambda p: affine(sip(p))
487 )
490class ScaledPolynomialTransformFitterTestCase(lsst.utils.tests.TestCase):
492 def setUp(self):
493 np.random.seed(50)
495 def testFromMatches(self):
496 # Setup artifical matches that correspond to a known (random) PolynomialTransform.
497 order = 3
498 truePoly = makeRandomPolynomialTransform(order)
499 crval = lsst.geom.SpherePoint(35.0, 10.0, lsst.geom.degrees)
500 crpix = lsst.geom.Point2D(50, 50)
501 cd = lsst.geom.LinearTransform.makeScaling((0.2*lsst.geom.arcseconds).asDegrees()).getMatrix()
502 initialWcs = lsst.afw.geom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cd)
503 bbox = lsst.geom.Box2D(
504 crval.getPosition(lsst.geom.arcseconds) - lsst.geom.Extent2D(20, 20),
505 crval.getPosition(lsst.geom.arcseconds) + lsst.geom.Extent2D(20, 20),
506 )
507 srcSchema = lsst.afw.table.SourceTable.makeMinimalSchema()
508 srcPosKey = lsst.afw.table.Point2DKey.addFields(srcSchema, "pos", "source position", "pix")
509 srcErrKey = lsst.afw.table.CovarianceMatrix2fKey.addFields(srcSchema, "pos",
510 ["x", "y"], ["pix", "pix"])
511 srcSchema.getAliasMap().set("slot_Centroid", "pos")
512 nPoints = 10
513 trueSrc = lsst.afw.table.SourceCatalog(srcSchema)
514 trueSrc.reserve(nPoints)
515 measSrc = lsst.afw.table.SourceCatalog(srcSchema)
516 measSrc.reserve(nPoints)
517 ref = lsst.afw.table.SimpleCatalog(lsst.afw.table.SimpleTable.makeMinimalSchema())
518 ref.reserve(nPoints)
519 refCoordKey = ref.getCoordKey()
520 errScaling = 1E-14
521 matches = []
522 initialIwcToSky = lsst.afw.geom.getIntermediateWorldCoordsToSky(initialWcs)
523 for i in range(nPoints):
524 refRec = ref.addNew()
525 raDeg, decDeg = (
526 np.random.uniform(low=bbox.getMinX(), high=bbox.getMaxX()),
527 np.random.uniform(low=bbox.getMinY(), high=bbox.getMaxY()),
528 )
529 skyCoord = lsst.geom.SpherePoint(raDeg, decDeg, lsst.geom.arcseconds)
530 refRec.set(refCoordKey, skyCoord)
531 trueRec = trueSrc.addNew()
532 truePos = truePoly(initialIwcToSky.applyInverse(skyCoord))
533 trueRec.set(srcPosKey, truePos)
534 measRec = measSrc.addNew()
535 covSqrt = np.random.randn(3, 2)
536 cov = (errScaling*(np.dot(covSqrt.transpose(), covSqrt)
537 + np.diag([1.0, 1.0]))).astype(np.float32)
538 # We don't actually perturb positions according to noise level, as
539 # this makes it much harder to test that the result agrees with
540 # what we put in.
541 measPos = truePos
542 measRec.set(srcPosKey, measPos)
543 measRec.set(srcErrKey, cov)
544 match = lsst.afw.table.ReferenceMatch(refRec, measRec, (measPos - truePos).computeNorm())
545 matches.append(match)
546 # Construct a fitter, and verify that the internal catalog it constructs is what we expect.
547 fitter = ScaledPolynomialTransformFitter.fromMatches(order, matches, initialWcs, 0.0)
548 expected = lsst.meas.astrom.compose(
549 fitter.getOutputScaling(),
550 lsst.meas.astrom.compose(truePoly, fitter.getInputScaling().inverted())
551 )
552 data = fitter.getData()
553 dataOutKey = lsst.afw.table.Point2DKey(data.schema["src"])
554 dataInKey = lsst.afw.table.Point2DKey(data.schema["intermediate"])
555 dataErrKey = lsst.afw.table.CovarianceMatrix2fKey(data.schema["src"], ["x", "y"])
556 scaledInBBox = lsst.geom.Box2D()
557 scaledOutBBox = lsst.geom.Box2D()
558 vandermonde = np.zeros((nPoints, (order + 1)*(order + 2)//2), dtype=float)
559 for i, (match, dataRec, trueRec) in enumerate(zip(matches, data, trueSrc)):
560 self.assertEqual(match.second.getX(), dataRec.get("src_x"))
561 self.assertEqual(match.second.getY(), dataRec.get("src_y"))
562 self.assertEqual(match.first.getId(), dataRec.get("ref_id"))
563 self.assertEqual(match.second.getId(), dataRec.get("src_id"))
564 refPos = initialIwcToSky.applyInverse(match.first.getCoord())
565 self.assertEqual(refPos.getX(), dataRec.get("intermediate_x"))
566 self.assertEqual(refPos.getY(), dataRec.get("intermediate_y"))
567 self.assertFloatsAlmostEqual(match.second.get(srcErrKey), dataRec.get(dataErrKey), rtol=1E-7)
568 scaledIn = fitter.getInputScaling()(dataRec.get(dataInKey))
569 scaledOut = fitter.getOutputScaling()(dataRec.get(dataOutKey))
570 scaledInBBox.include(scaledIn)
571 scaledOutBBox.include(scaledOut)
572 self.assertFloatsAlmostEqual(np.array(expected(scaledIn)), np.array(scaledOut), rtol=1E-7)
573 j = 0
574 for n in range(order + 1):
575 for p in range(n + 1):
576 q = n - p
577 vandermonde[i, j] = scaledIn.getX()**p * scaledIn.getY()**q
578 j += 1
579 # Verify that scaling transforms move inputs and outputs into [-1, 1]
580 self.assertFloatsAlmostEqual(scaledInBBox.getMinX(), -1.0, rtol=1E-12)
581 self.assertFloatsAlmostEqual(scaledInBBox.getMinY(), -1.0, rtol=1E-12)
582 self.assertFloatsAlmostEqual(scaledInBBox.getMaxX(), 1.0, rtol=1E-12)
583 self.assertFloatsAlmostEqual(scaledInBBox.getMaxY(), 1.0, rtol=1E-12)
584 self.assertFloatsAlmostEqual(scaledOutBBox.getMinX(), -1.0, rtol=1E-12)
585 self.assertFloatsAlmostEqual(scaledOutBBox.getMinY(), -1.0, rtol=1E-12)
586 self.assertFloatsAlmostEqual(scaledOutBBox.getMaxX(), 1.0, rtol=1E-12)
587 self.assertFloatsAlmostEqual(scaledOutBBox.getMaxY(), 1.0, rtol=1E-12)
588 # Run the fitter, and check that we get out approximately what we put in.
589 fitter.fit(order)
590 fitter.updateModel()
591 # Check the transformed input points.
592 self.assertFloatsAlmostEqual(data.get("model_x"), trueSrc.getX(), rtol=1E-15)
593 self.assertFloatsAlmostEqual(data.get("model_y"), trueSrc.getY(), rtol=1E-15)
594 # Check the actual transform's coefficients (after composing in the scaling, which is
595 # a lot of the reason we lose a lot of precision here).
596 fittedPoly = lsst.meas.astrom.PolynomialTransform.convert(fitter.getTransform())
597 self.assertFloatsAlmostEqual(fittedPoly.getXCoeffs(), truePoly.getXCoeffs(), rtol=1E-5, atol=1E-5)
598 self.assertFloatsAlmostEqual(fittedPoly.getYCoeffs(), truePoly.getYCoeffs(), rtol=1E-5, atol=1E-5)
600 def testFromGrid(self):
601 outOrder = 8
602 inOrder = 2
603 toInvert = makeRandomScaledPolynomialTransform(inOrder)
604 bbox = lsst.geom.Box2D(lsst.geom.Point2D(432, -671), lsst.geom.Point2D(527, -463))
605 fitter = ScaledPolynomialTransformFitter.fromGrid(outOrder, bbox, 50, 50, toInvert)
606 fitter.fit(outOrder)
607 fitter.updateModel()
608 data = fitter.getData()
609 result = fitter.getTransform()
610 inputKey = lsst.afw.table.Point2DKey(data.schema["input"])
611 outputKey = lsst.afw.table.Point2DKey(data.schema["output"])
612 for record in data:
613 self.assertFloatsAlmostEqual(np.array(record.get(inputKey)),
614 np.array(toInvert(record.get(outputKey))))
615 self.assertFloatsAlmostEqual(np.array(result(record.get(inputKey))),
616 np.array(record.get(outputKey)),
617 rtol=1E-2) # even at much higher order, inverse can't be perfect.
620class MemoryTester(lsst.utils.tests.MemoryTestCase):
621 pass
624def setup_module(module):
625 lsst.utils.tests.init()
628if __name__ == "__main__": 628 ↛ 629line 628 didn't jump to line 629, because the condition on line 628 was never true
629 lsst.utils.tests.init()
630 unittest.main()