Coverage for tests/test_fitAffineWcsTask.py: 20%
137 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:38 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-27 03:38 -0700
1# This file is part of meas_astrom.
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 math
23import unittest
25import numpy as np
27import lsst.pipe.base
28import lsst.utils.tests
29import lsst.geom
30import lsst.afw.geom as afwGeom
31import lsst.afw.table as afwTable
32from lsst.meas.algorithms import convertReferenceCatalog
33from lsst.meas.base import SingleFrameMeasurementTask
34from lsst.meas.astrom import FitAffineWcsTask, TransformedSkyWcsMaker
37class BaseTestCase:
39 """A test case for FitAffineWcsTask.
41 Use involves setting one class attribute:
42 * MatchClass: match class, e.g. ReferenceMatch or SourceMatch
43 """
44 MatchClass = None
46 def setUp(self):
47 crval = lsst.geom.SpherePoint(44, 45, lsst.geom.degrees)
48 crpix = lsst.geom.Point2D(15000, 4000)
50 scale = 1 * lsst.geom.arcseconds
51 cdMatrix = afwGeom.makeCdMatrix(scale=scale, flipX=True)
52 self.tanWcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix)
53 self.loadData()
55 def loadData(self, rangePix=3000, numPoints=25):
56 """Load catalogs and make the match list
58 This is a separate function so data can be reloaded if fitting more than once
59 (each time a WCS is fit it may update the source catalog, reference catalog and match list)
60 """
61 refSchema = convertReferenceCatalog._makeSchema(filterNameList=["r"], addIsPhotometric=True,
62 addCentroid=True)
63 self.refCat = afwTable.SimpleCatalog(refSchema)
64 srcSchema = afwTable.SourceTable.makeMinimalSchema()
65 SingleFrameMeasurementTask(schema=srcSchema)
66 afwTable.CoordKey.addErrorFields(srcSchema)
67 self.srcCoordKey = afwTable.CoordKey(srcSchema["coord"])
68 self.srcCentroidKey = afwTable.Point2DKey(srcSchema["slot_Centroid"])
69 self.srcCentroidKey_xErr = srcSchema["slot_Centroid_xErr"].asKey()
70 self.srcCentroidKey_yErr = srcSchema["slot_Centroid_yErr"].asKey()
71 self.sourceCat = afwTable.SourceCatalog(srcSchema)
73 self.matches = []
75 for i in np.linspace(0., rangePix, numPoints):
76 for j in np.linspace(0., rangePix, numPoints):
77 src = self.sourceCat.addNew()
78 refObj = self.refCat.addNew()
80 src.set(self.srcCentroidKey, lsst.geom.Point2D(i, j))
81 src.set(self.srcCentroidKey_xErr, 0.1)
82 src.set(self.srcCentroidKey_yErr, 0.1)
84 c = self.tanWcs.pixelToSky(i, j)
85 refObj.setCoord(c)
87 self.matches.append(self.MatchClass(refObj, src, 0.0))
89 def tearDown(self):
90 del self.refCat
91 del self.sourceCat
92 del self.matches
93 del self.tanWcs
95 def checkResults(self, fitRes, catsUpdated):
96 """Check results.
97 """
98 self.assertLess(fitRes.scatterOnSky.asArcseconds(), 0.001)
99 affineWcs = fitRes.wcs
100 maxAngSep = 0*lsst.geom.radians
101 maxPixSep = 0
102 refCoordKey = afwTable.CoordKey(self.refCat.schema["coord"])
103 if catsUpdated:
104 refCentroidKey = afwTable.Point2DKey(self.refCat.schema["centroid"])
105 maxDistErr = 0*lsst.geom.radians
106 for refObj, src, distRad in self.matches:
107 srcPixPos = src.get(self.srcCentroidKey)
108 refCoord = refObj.get(refCoordKey)
109 if catsUpdated:
110 refPixPos = refObj.get(refCentroidKey)
111 srcCoord = src.get(self.srcCoordKey)
112 else:
113 refPixPos = affineWcs.skyToPixel(refCoord)
114 srcCoord = affineWcs.pixelToSky(srcPixPos)
116 angSep = refCoord.separation(srcCoord)
117 dist = distRad*lsst.geom.radians
118 distErr = abs(dist - angSep)
119 maxDistErr = max(maxDistErr, distErr)
120 maxAngSep = max(maxAngSep, angSep)
122 pixSep = math.hypot(*(srcPixPos - refPixPos))
123 maxPixSep = max(maxPixSep, pixSep)
125 print("max angular separation = %0.4f arcsec" % (maxAngSep.asArcseconds(),))
126 print("max pixel separation = %0.3f" % (maxPixSep,))
127 self.assertLess(maxAngSep.asArcseconds(), 0.001)
128 self.assertLess(maxPixSep, 0.005)
129 if catsUpdated:
130 allowedDistErr = 1e-7
131 else:
132 allowedDistErr = 0.001
133 self.assertLess(maxDistErr.asArcseconds(), allowedDistErr,
134 "Computed distance in match list is off by %s arcsec" % (maxDistErr.asArcseconds(),))
136 def doTest(self, name, func):
137 """Apply func(x, y) to each source in self.sourceCat, then fit and
138 check the resulting WCS.
139 """
140 bbox = lsst.geom.Box2I()
141 for refObj, src, d in self.matches:
142 origPos = src.get(self.srcCentroidKey)
143 x, y = func(*origPos)
144 distortedPos = lsst.geom.Point2D(x, y)
145 src.set(self.srcCentroidKey, distortedPos)
146 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(distortedPos)))
148 fitter = FitAffineWcsTask()
149 fitRes = fitter.fitWcs(
150 matches=self.matches,
151 initWcs=self.tanWcs,
152 bbox=bbox,
153 refCat=self.refCat,
154 sourceCat=self.sourceCat,
155 )
157 self.checkResults(fitRes, catsUpdated=True)
159 def doTestAffine(self, name, offset, matrix):
160 """Apply func(x, y) to each source in self.sourceCat, then fit and
161 check the resulting WCS.
162 """
163 wcsMaker = TransformedSkyWcsMaker(self.tanWcs)
165 newWcs = wcsMaker.makeWcs(offset, matrix)
166 bbox = lsst.geom.Box2I()
167 for refObj, src, d in self.matches:
168 origPos = src.get(self.srcCentroidKey)
169 newCoord = newWcs.pixelToSky(origPos)
170 src.setCoord(newCoord)
171 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(origPos)))
173 fitter = FitAffineWcsTask()
174 fitRes = fitter.fitWcs(
175 matches=self.matches,
176 initWcs=newWcs,
177 bbox=bbox,
178 refCat=self.refCat,
179 sourceCat=self.sourceCat,
180 )
182 self.checkResults(fitRes, catsUpdated=True)
184 def doTestAffineReverse(self, name, offset, matrix):
185 """Apply func(x, y) to each source in self.sourceCat, then fit and
186 check the resulting WCS.
187 """
188 wcsMaker = TransformedSkyWcsMaker(self.tanWcs)
190 newWcs = wcsMaker.makeWcs(offset, matrix)
191 bbox = lsst.geom.Box2I()
192 for refObj, src, d in self.matches:
193 origCoord = src.get(self.srcCoordKey)
194 newCentroid = newWcs.skyToPixel(origCoord)
195 src.set(self.srcCentroidKey, newCentroid)
196 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(newCentroid)))
198 fitter = FitAffineWcsTask()
199 fitRes = fitter.fitWcs(
200 matches=self.matches,
201 initWcs=self.tanWcs,
202 bbox=bbox,
203 refCat=self.refCat,
204 sourceCat=self.sourceCat,
205 )
207 self.checkResults(fitRes, catsUpdated=True)
210class SideLoadTestCases:
212 """Base class implementations of testing methods.
214 Explicitly does not inherit from unittest.TestCase"""
216 def testTrivial(self):
217 """Add no distortion"""
218 self.doTest("testTrivial", lambda x, y: (x, y))
220 def testOffset(self):
221 """Add an offset"""
222 self.doTest("testOffset", lambda x, y: (x + 5, y + 7))
224 def testSkyOffset(self):
225 """Add an on sky offset"""
226 self.doTestAffine("testSkyOffset",
227 [77, -200],
228 np.array([[1.0, 0.0], [0.0, 1.0]]))
229 self.doTestAffineReverse("testSkyOffsetRev",
230 [77, -200],
231 np.array([[1.0, 0.0], [0.0, 1.0]]))
233 def testAffine(self):
234 """Add an Affine (shear + scale + rot) distortion"""
235 self.doTestAffine("testAffine",
236 [0, 0],
237 np.array([[0.4, 0.1], [-0.21, 2.0]]))
238 self.doTestAffineReverse("testAffineRev",
239 [0, 0],
240 np.array([[0.4, 0.1], [-0.21, 2.0]]))
242 def testAffineAndOffset(self):
243 """Add a transform and offset"""
244 self.doTestAffine("testAffineAndOffset",
245 [30, 100],
246 np.array([[0.5, 0.01], [-0.2, 0.3]]))
247 self.doTestAffineReverse("testAffineAndOffsetRev",
248 [30, 100],
249 np.array([[0.5, 0.01], [-0.2, 0.3]]))
252# The test classes inherit from two base classes and differ in the match
253# class being used.
255class FitAffineWcsTaskTestCaseReferenceMatch(BaseTestCase,
256 SideLoadTestCases,
257 lsst.utils.tests.TestCase):
258 MatchClass = afwTable.ReferenceMatch
261class MemoryTester(lsst.utils.tests.MemoryTestCase):
262 pass
265def setup_module(module):
266 lsst.utils.tests.init()
269if __name__ == "__main__": 269 ↛ 270line 269 didn't jump to line 270, because the condition on line 269 was never true
270 lsst.utils.tests.init()
271 unittest.main()