Coverage for tests/test_matchOptimisticB.py: 18%
134 statements
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-26 02:37 -0700
« prev ^ index » next coverage.py v7.2.6, created at 2023-05-26 02:37 -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 convertReferenceCatalog
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 = convertReferenceCatalog._makeSchema(filterNameList=["r"], addCentroid=True)
154 refCat = afwTable.SimpleCatalog(minimalPosRefSchema)
155 for source in sourceCat:
156 refObj = refCat.addNew()
157 refObj.setCoord(source.getCoord())
158 refObj.set("centroid_x", source.getX())
159 refObj.set("centroid_y", source.getY())
160 refObj.set("hasCentroid", True)
161 refObj.set("r_flux", source.get("slot_ApFlux_instFlux"))
162 refObj.set("r_fluxErr", source.get("slot_ApFlux_instFluxErr"))
163 refObj.setId(source.getId())
164 return refCat
166 def loadSourceCatalog(self, filename):
167 """Load a list of xy points from a file, set coord, and return a SourceSet of points
168 """
169 sourceCat = afwTable.SourceCatalog.readFits(filename)
170 aliasMap = sourceCat.schema.getAliasMap()
171 aliasMap.set("slot_ApFlux", "base_PsfFlux")
172 instFluxKey = sourceCat.schema["slot_ApFlux_instFlux"].asKey()
173 instFluxErrKey = sourceCat.schema["slot_ApFlux_instFluxErr"].asKey()
175 # print("schema=", sourceCat.schema)
177 # Source x,y positions are ~ (500,1500) x (500,1500)
178 centroidKey = sourceCat.table.getCentroidSlot().getMeasKey()
179 for src in sourceCat:
180 adjCentroid = src.get(centroidKey) - lsst.geom.Extent2D(500, 500)
181 src.set(centroidKey, adjCentroid)
182 src.set(instFluxKey, 1000)
183 src.set(instFluxErrKey, 1)
185 # Set catalog coord
186 for src in sourceCat:
187 src.updateCoord(self.wcs)
188 return sourceCat
190 def testArgumentErrors(self):
191 """Test argument sanity checking in matchOptimisticB
192 """
193 matchControl = matchOptimisticB.MatchOptimisticBControl()
195 sourceCat = self.loadSourceCatalog(self.filename)
196 emptySourceCat = afwTable.SourceCatalog(sourceCat.schema)
198 refCat = self.computePosRefCatalog(sourceCat)
199 emptyRefCat = afwTable.SimpleCatalog(refCat.schema)
201 with self.assertRaises(pexExcept.InvalidParameterError):
202 matchOptimisticB.matchOptimisticB(
203 emptyRefCat,
204 sourceCat,
205 matchControl,
206 self.wcs,
207 0,
208 )
209 with self.assertRaises(pexExcept.InvalidParameterError):
210 matchOptimisticB.matchOptimisticB(
211 refCat,
212 emptySourceCat,
213 matchControl,
214 self.wcs,
215 0,
216 )
217 with self.assertRaises(pexExcept.InvalidParameterError):
218 matchOptimisticB.matchOptimisticB(
219 refCat,
220 sourceCat,
221 matchControl,
222 self.wcs,
223 len(refCat),
224 )
225 with self.assertRaises(pexExcept.InvalidParameterError):
226 matchOptimisticB.matchOptimisticB(
227 refCat,
228 sourceCat,
229 matchControl,
230 self.wcs,
231 -1,
232 )
234 def testConfigPickle(self):
235 """Test that we can pickle the Config
237 This is required for use in singleFrameDriver.
238 See DM-18314.
239 """
240 config = pickle.loads(pickle.dumps(self.config))
241 self.assertEqual(config, self.config)
244class MemoryTester(lsst.utils.tests.MemoryTestCase):
245 pass
248def setup_module(module):
249 lsst.utils.tests.init()
252if __name__ == "__main__": 252 ↛ 254line 252 didn't jump to line 254, because the condition on line 252 was never true
254 lsst.utils.tests.init()
255 unittest.main()