Coverage for tests/test_matchOptimisticB.py: 20%
134 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-21 01:51 -0700
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-21 01:51 -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 math
24import os
25import unittest
26import pickle
28import lsst.geom
29import lsst.afw.geom as afwGeom
30import lsst.afw.table as afwTable
31import lsst.utils.tests
32import lsst.pex.exceptions as pexExcept
33from lsst.meas.algorithms import LoadReferenceObjectsTask
34import lsst.meas.astrom.sip.genDistortedImage as distort
35import lsst.meas.astrom as measAstrom
36import lsst.meas.astrom.matchOptimisticB as matchOptimisticB
39class TestMatchOptimisticB(unittest.TestCase):
41 def setUp(self):
43 self.config = measAstrom.MatchOptimisticBTask.ConfigClass()
44 self.matchOptimisticB = measAstrom.MatchOptimisticBTask(config=self.config)
45 self.wcs = afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(791.4, 559.7),
46 crval=lsst.geom.SpherePoint(36.930640, -4.939560, lsst.geom.degrees),
47 cdMatrix=afwGeom.makeCdMatrix(scale=5.17e-5*lsst.geom.degrees))
48 self.distortedWcs = self.wcs
50 self.filename = os.path.join(os.path.dirname(__file__), "cat.xy.fits")
51 self.tolArcsec = .4
52 self.tolPixel = .1
54 def tearDown(self):
55 del self.config
56 del self.matchOptimisticB
57 del self.wcs
58 del self.distortedWcs
60 def testLinearXDistort(self):
61 self.singleTestInstance(self.filename, distort.linearXDistort)
63 def testLinearYDistort(self):
64 self.singleTestInstance(self.filename, distort.linearYDistort)
66 def testQuadraticDistort(self):
67 self.singleTestInstance(self.filename, distort.quadraticDistort)
69 def testLargeDistortion(self):
70 # This transform is about as extreme as I can get:
71 # using 0.0005 in the last value appears to produce numerical issues.
72 # It produces a maximum deviation of 459 pixels, which should be sufficient.
73 pixelsToTanPixels = afwGeom.makeRadialTransform([0.0, 1.1, 0.0004])
74 self.distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels,
75 wcs=self.wcs,
76 modifyActualPixels=False)
78 def applyDistortion(src):
79 out = src.table.copyRecord(src)
80 out.set(out.table.getCentroidSlot().getMeasKey(),
81 pixelsToTanPixels.applyInverse(src.getCentroid()))
82 return out
84 self.singleTestInstance(self.filename, applyDistortion)
86 def singleTestInstance(self, filename, distortFunc, doPlot=False):
87 sourceCat = self.loadSourceCatalog(self.filename)
88 refCat = self.computePosRefCatalog(sourceCat)
90 # Apply source selector to sourceCat, using the astrometry config defaults
91 tempConfig = measAstrom.AstrometryTask.ConfigClass()
92 tempConfig.matcher.retarget(measAstrom.MatchOptimisticBTask)
93 tempConfig.sourceSelector["matcher"].excludePixelFlags = False
94 tempSolver = measAstrom.AstrometryTask(config=tempConfig, refObjLoader=None)
95 sourceSelection = tempSolver.sourceSelector.run(sourceCat)
97 distortedCat = distort.distortList(sourceSelection.sourceCat, distortFunc)
99 if doPlot:
100 import matplotlib.pyplot as plt
101 undistorted = [self.wcs.skyToPixel(self.distortedWcs.pixelToSky(ss.getCentroid())) for
102 ss in distortedCat]
103 refs = [self.wcs.skyToPixel(ss.getCoord()) for ss in refCat]
105 def plot(catalog, symbol):
106 plt.plot([ss.getX() for ss in catalog], [ss.getY() for ss in catalog], symbol)
108 # plot(sourceCat, 'k+') # Original positions: black +
109 plot(distortedCat, 'b+') # Distorted positions: blue +
110 plot(undistorted, 'g+') # Undistorted positions: green +
111 plot(refs, 'rx') # Reference catalog: red x
112 # The green + should overlap with the red x, because that's how matchOptimisticB does it.
113 # The black + happens to overlap with those also, but that's beside the point.
114 plt.show()
116 sourceCat = distortedCat
118 matchRes = self.matchOptimisticB.matchObjectsToSources(
119 refCat=refCat,
120 sourceCat=sourceCat,
121 wcs=self.distortedWcs,
122 sourceFluxField='slot_ApFlux_instFlux',
123 refFluxField="r_flux",
124 )
125 matches = matchRes.matches
126 if doPlot:
127 measAstrom.plotAstrometry(matches=matches, refCat=refCat, sourceCat=sourceCat)
128 self.assertEqual(len(matches), 183)
130 refCoordKey = afwTable.CoordKey(refCat.schema["coord"])
131 srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"])
132 refCentroidKey = afwTable.Point2DKey(refCat.getSchema()["centroid"])
133 maxDistErr = 0*lsst.geom.radians
134 for refObj, source, distRad in matches:
135 sourceCoord = source.get(srcCoordKey)
136 refCoord = refObj.get(refCoordKey)
137 predDist = sourceCoord.separation(refCoord)
138 distErr = abs(predDist - distRad*lsst.geom.radians)
139 maxDistErr = max(distErr, maxDistErr)
141 if refObj.getId() != source.getId():
142 refCentroid = refObj.get(refCentroidKey)
143 sourceCentroid = source.getCentroid()
144 radius = math.hypot(*(refCentroid - sourceCentroid))
145 self.fail("ID mismatch: %s at %s != %s at %s; error = %0.1f pix" %
146 (refObj.getId(), refCentroid, source.getId(), sourceCentroid, radius))
148 self.assertLess(maxDistErr.asArcseconds(), 1e-7)
150 def computePosRefCatalog(self, sourceCat):
151 """Generate a position reference catalog from a source catalog
152 """
153 minimalPosRefSchema = LoadReferenceObjectsTask.makeMinimalSchema(filterNameList=["r"],
154 addCentroid=True)
155 refCat = afwTable.SimpleCatalog(minimalPosRefSchema)
156 for source in sourceCat:
157 refObj = refCat.addNew()
158 refObj.setCoord(source.getCoord())
159 refObj.set("centroid_x", source.getX())
160 refObj.set("centroid_y", source.getY())
161 refObj.set("hasCentroid", True)
162 refObj.set("r_flux", source.get("slot_ApFlux_instFlux"))
163 refObj.set("r_fluxErr", source.get("slot_ApFlux_instFluxErr"))
164 refObj.setId(source.getId())
165 return refCat
167 def loadSourceCatalog(self, filename):
168 """Load a list of xy points from a file, set coord, and return a SourceSet of points
169 """
170 sourceCat = afwTable.SourceCatalog.readFits(filename)
171 aliasMap = sourceCat.schema.getAliasMap()
172 aliasMap.set("slot_ApFlux", "base_PsfFlux")
173 instFluxKey = sourceCat.schema["slot_ApFlux_instFlux"].asKey()
174 instFluxErrKey = sourceCat.schema["slot_ApFlux_instFluxErr"].asKey()
176 # print("schema=", sourceCat.schema)
178 # Source x,y positions are ~ (500,1500) x (500,1500)
179 centroidKey = sourceCat.table.getCentroidSlot().getMeasKey()
180 for src in sourceCat:
181 adjCentroid = src.get(centroidKey) - lsst.geom.Extent2D(500, 500)
182 src.set(centroidKey, adjCentroid)
183 src.set(instFluxKey, 1000)
184 src.set(instFluxErrKey, 1)
186 # Set catalog coord
187 for src in sourceCat:
188 src.updateCoord(self.wcs)
189 return sourceCat
191 def testArgumentErrors(self):
192 """Test argument sanity checking in matchOptimisticB
193 """
194 matchControl = matchOptimisticB.MatchOptimisticBControl()
196 sourceCat = self.loadSourceCatalog(self.filename)
197 emptySourceCat = afwTable.SourceCatalog(sourceCat.schema)
199 refCat = self.computePosRefCatalog(sourceCat)
200 emptyRefCat = afwTable.SimpleCatalog(refCat.schema)
202 with self.assertRaises(pexExcept.InvalidParameterError):
203 matchOptimisticB.matchOptimisticB(
204 emptyRefCat,
205 sourceCat,
206 matchControl,
207 self.wcs,
208 0,
209 )
210 with self.assertRaises(pexExcept.InvalidParameterError):
211 matchOptimisticB.matchOptimisticB(
212 refCat,
213 emptySourceCat,
214 matchControl,
215 self.wcs,
216 0,
217 )
218 with self.assertRaises(pexExcept.InvalidParameterError):
219 matchOptimisticB.matchOptimisticB(
220 refCat,
221 sourceCat,
222 matchControl,
223 self.wcs,
224 len(refCat),
225 )
226 with self.assertRaises(pexExcept.InvalidParameterError):
227 matchOptimisticB.matchOptimisticB(
228 refCat,
229 sourceCat,
230 matchControl,
231 self.wcs,
232 -1,
233 )
235 def testConfigPickle(self):
236 """Test that we can pickle the Config
238 This is required for use in singleFrameDriver.
239 See DM-18314.
240 """
241 config = pickle.loads(pickle.dumps(self.config))
242 self.assertEqual(config, self.config)
245class MemoryTester(lsst.utils.tests.MemoryTestCase):
246 pass
249def setup_module(module):
250 lsst.utils.tests.init()
253if __name__ == "__main__": 253 ↛ 255line 253 didn't jump to line 255, because the condition on line 253 was never true
255 lsst.utils.tests.init()
256 unittest.main()