Coverage for tests/test_matchPessimisticB.py: 16%
152 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-09 03:57 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-09 03:57 -0800
3#
4# LSST Data Management System
5# Copyright 2008, 2009, 2010 LSST Corporation.
6#
7# This product includes software developed by the
8# LSST Project (http://www.lsst.org/).
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the LSST License Statement and
21# the GNU General Public License along with this program. If not,
22# see <http://www.lsstcorp.org/LegalNotices/>.
23#
24import math
25import os
26import unittest
28import numpy as np
30import lsst.geom
31import lsst.afw.geom as afwGeom
32import lsst.afw.table as afwTable
33import lsst.utils.tests
34from lsst.meas.algorithms import convertReferenceCatalog
35import lsst.meas.astrom.sip.genDistortedImage as distort
36import lsst.meas.astrom as measAstrom
39class TestMatchPessimisticB(unittest.TestCase):
41 def setUp(self):
43 np.random.seed(12345)
45 self.config = measAstrom.MatchPessimisticBTask.ConfigClass()
46 # Value below is to assure all matches are selected. The
47 # original test is set for a 3 arcsecond max match distance
48 # using matchOptimisticB.
49 self.config.minMatchDistPixels = 2.0
50 self.MatchPessimisticB = measAstrom.MatchPessimisticBTask(
51 config=self.config)
53 self.wcs = afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(791.4, 559.7),
54 crval=lsst.geom.SpherePoint(36.930640, -4.939560, lsst.geom.degrees),
55 cdMatrix=afwGeom.makeCdMatrix(scale=5.17e-5*lsst.geom.degrees))
56 self.distortedWcs = self.wcs
58 self.filename = os.path.join(os.path.dirname(__file__), "cat.xy.fits")
59 self.tolArcsec = .4
60 self.tolPixel = .1
62 # 3 of the objects are removed by the source selector and are used in
63 # matching hence the 183 number vs the total of 186. This is also why
64 # these three objects are missing in the testReferenceFilter test.
65 self.expectedMatches = 183
67 def tearDown(self):
68 del self.config
69 del self.MatchPessimisticB
70 del self.wcs
71 del self.distortedWcs
73 def testLinearXDistort(self):
74 self.singleTestInstance(self.filename, distort.linearXDistort)
76 def testLinearYDistort(self):
77 self.singleTestInstance(self.filename, distort.linearYDistort)
79 def testQuadraticDistort(self):
80 self.singleTestInstance(self.filename, distort.quadraticDistort)
82 def testLargeDistortion(self):
83 # This transform is about as extreme as I can get:
84 # using 0.0005 in the last value appears to produce numerical issues.
86 # It produces a maximum deviation of 459 pixels, which should be
87 # sufficient.
88 pixelsToTanPixels = afwGeom.makeRadialTransform([0.0, 1.1, 0.0004])
89 self.distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels,
90 wcs=self.wcs,
91 modifyActualPixels=False)
93 def applyDistortion(src):
94 out = src.table.copyRecord(src)
95 out.set(out.table.getCentroidSlot().getMeasKey(),
96 pixelsToTanPixels.applyInverse(src.getCentroid()))
97 return out
99 self.singleTestInstance(self.filename, applyDistortion)
101 def singleTestInstance(self, filename, distortFunc, doPlot=False):
102 sourceCat = self.loadSourceCatalog(self.filename)
103 refCat = self.computePosRefCatalog(sourceCat)
105 # Apply source selector to sourceCat, using the astrometry config defaults
106 tempConfig = measAstrom.AstrometryTask.ConfigClass()
107 tempSolver = measAstrom.AstrometryTask(config=tempConfig, refObjLoader=None)
108 sourceSelection = tempSolver.sourceSelector.run(sourceCat)
110 distortedCat = distort.distortList(sourceSelection.sourceCat, distortFunc)
112 if doPlot:
113 import matplotlib.pyplot as plt
115 undistorted = [self.wcs.skyToPixel(self.distortedWcs.pixelToSky(ss.getCentroid()))
116 for ss in distortedCat]
117 refs = [self.wcs.skyToPixel(ss.getCoord()) for ss in refCat]
119 def plot(catalog, symbol):
120 plt.plot([ss.getX() for ss in catalog],
121 [ss.getY() for ss in catalog], symbol)
123 plot(distortedCat, 'b+') # Distorted positions: blue +
124 plot(undistorted, 'g+') # Undistorted positions: green +
125 plot(refs, 'rx') # Reference catalog: red x
126 # The green + should overlap with the red x, because that's how
127 # MatchPessimisticB does it.
129 plt.show()
131 sourceCat = distortedCat
133 matchRes = self.MatchPessimisticB.matchObjectsToSources(
134 refCat=refCat,
135 sourceCat=sourceCat,
136 wcs=self.distortedWcs,
137 sourceFluxField='slot_ApFlux_instFlux',
138 refFluxField="r_flux",
139 )
140 matches = matchRes.matches
141 if doPlot:
142 measAstrom.plotAstrometry(matches=matches, refCat=refCat,
143 sourceCat=sourceCat)
144 self.assertEqual(len(matches), self.expectedMatches)
146 refCoordKey = afwTable.CoordKey(refCat.schema["coord"])
147 srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"])
148 refCentroidKey = afwTable.Point2DKey(refCat.getSchema()["centroid"])
149 maxDistErr = 0*lsst.geom.radians
151 for refObj, source, distRad in matches:
152 sourceCoord = source.get(srcCoordKey)
153 refCoord = refObj.get(refCoordKey)
154 predDist = sourceCoord.separation(refCoord)
155 distErr = abs(predDist - distRad*lsst.geom.radians)
156 maxDistErr = max(distErr, maxDistErr)
158 if refObj.getId() != source.getId():
159 refCentroid = refObj.get(refCentroidKey)
160 sourceCentroid = source.getCentroid()
161 radius = math.hypot(*(refCentroid - sourceCentroid))
162 self.fail(
163 "ID mismatch: %s at %s != %s at %s; error = %0.1f pix" %
164 (refObj.getId(), refCentroid, source.getId(),
165 sourceCentroid, radius))
167 self.assertLess(maxDistErr.asArcseconds(), 1e-7)
169 def testPassingMatcherState(self):
170 """Test that results of the matcher can be propagated to to in
171 subsequent iterations.
172 """
173 sourceCat = self.loadSourceCatalog(self.filename)
174 refCat = self.computePosRefCatalog(sourceCat)
176 # Apply source selector to sourceCat, using the astrometry config defaults
177 tempConfig = measAstrom.AstrometryTask.ConfigClass()
178 tempSolver = measAstrom.AstrometryTask(config=tempConfig, refObjLoader=None)
179 sourceSelection = tempSolver.sourceSelector.run(sourceCat)
181 distortedCat = distort.distortList(sourceSelection.sourceCat, distort.linearXDistort)
183 sourceCat = distortedCat
185 matchRes = self.MatchPessimisticB.matchObjectsToSources(
186 refCat=refCat,
187 sourceCat=sourceCat,
188 wcs=self.distortedWcs,
189 sourceFluxField='slot_ApFlux_instFlux',
190 refFluxField="r_flux",
191 )
193 maxShift = matchRes.match_tolerance.maxShift * 300
194 # Force the matcher to use a different pattern thatn the previous
195 # "iteration".
196 matchTol = measAstrom.MatchTolerancePessimistic(
197 maxMatchDist=matchRes.match_tolerance.maxMatchDist,
198 autoMaxMatchDist=matchRes.match_tolerance.autoMaxMatchDist,
199 maxShift=maxShift,
200 lastMatchedPattern=0,
201 failedPatternList=[0],
202 PPMbObj=matchRes.match_tolerance.PPMbObj,
203 )
205 matchRes = self.MatchPessimisticB.matchObjectsToSources(
206 refCat=refCat,
207 sourceCat=sourceCat,
208 wcs=self.distortedWcs,
209 sourceFluxField='slot_ApFlux_instFlux',
210 refFluxField="r_flux",
211 match_tolerance=matchTol,
212 )
214 self.assertEqual(len(matchRes.matches), self.expectedMatches)
215 self.assertLess(matchRes.match_tolerance.maxShift, maxShift)
216 self.assertEqual(matchRes.match_tolerance.lastMatchedPattern, 1)
217 self.assertIsNotNone(matchRes.match_tolerance.maxMatchDist)
218 self.assertIsNotNone(matchRes.match_tolerance.autoMaxMatchDist)
219 self.assertIsNotNone(matchRes.match_tolerance.lastMatchedPattern)
220 self.assertIsNotNone(matchRes.match_tolerance.failedPatternList)
221 self.assertIsNotNone(matchRes.match_tolerance.PPMbObj)
223 def testReferenceFilter(self):
224 """Test sub-selecting reference objects by flux."""
225 sourceCat = self.loadSourceCatalog(self.filename)
226 refCat = self.computePosRefCatalog(sourceCat)
228 # Apply source selector to sourceCat, using the astrometry config defaults
229 tempConfig = measAstrom.AstrometryTask.ConfigClass()
230 tempSolver = measAstrom.AstrometryTask(config=tempConfig, refObjLoader=None)
231 sourceSelection = tempSolver.sourceSelector.run(sourceCat)
233 distortedCat = distort.distortList(sourceSelection.sourceCat, distort.linearXDistort)
235 matchPessConfig = measAstrom.MatchPessimisticBTask.ConfigClass()
236 matchPessConfig.maxRefObjects = 150
237 matchPessConfig.minMatchDistPixels = 5.0
239 matchPess = measAstrom.MatchPessimisticBTask(config=matchPessConfig)
240 trimmedRefCat = matchPess._filterRefCat(refCat, 'r_flux')
241 self.assertEqual(len(trimmedRefCat), matchPessConfig.maxRefObjects)
243 matchRes = matchPess.matchObjectsToSources(
244 refCat=refCat,
245 sourceCat=distortedCat,
246 wcs=self.distortedWcs,
247 sourceFluxField='slot_ApFlux_instFlux',
248 refFluxField="r_flux",
249 )
251 self.assertEqual(len(matchRes.matches), matchPessConfig.maxRefObjects - 3)
253 def computePosRefCatalog(self, sourceCat):
254 """Generate a position reference catalog from a source catalog
255 """
256 minimalPosRefSchema = convertReferenceCatalog._makeSchema(filterNameList=["r"], addCentroid=True)
257 refCat = afwTable.SimpleCatalog(minimalPosRefSchema)
258 refCat.reserve(len(sourceCat))
259 for source in sourceCat:
260 refObj = refCat.addNew()
261 refObj.setCoord(source.getCoord())
262 refObj.set("centroid_x", source.getX())
263 refObj.set("centroid_y", source.getY())
264 refObj.set("hasCentroid", True)
265 refObj.set("r_flux", np.random.uniform(1, 10000))
266 refObj.set("r_fluxErr", source.get("slot_ApFlux_instFluxErr"))
267 refObj.setId(source.getId())
268 return refCat
270 def loadSourceCatalog(self, filename):
271 """Load a list of xy points from a file, set coord, and return a
272 SourceSet of points
274 """
275 sourceCat = afwTable.SourceCatalog.readFits(filename)
276 aliasMap = sourceCat.schema.getAliasMap()
277 aliasMap.set("slot_ApFlux", "base_PsfFlux")
278 instFluxKey = sourceCat.schema["slot_ApFlux_instFlux"].asKey()
279 instFluxErrKey = sourceCat.schema["slot_ApFlux_instFluxErr"].asKey()
281 # Source x,y positions are ~ (500,1500) x (500,1500)
282 centroidKey = sourceCat.table.getCentroidSlot().getMeasKey()
283 for src in sourceCat:
284 adjCentroid = src.get(centroidKey) - lsst.geom.Extent2D(500, 500)
285 src.set(centroidKey, adjCentroid)
286 src.set(instFluxKey, 1000)
287 src.set(instFluxErrKey, 1)
289 # Set catalog coord
290 for src in sourceCat:
291 src.updateCoord(self.wcs)
292 return sourceCat
295class MemoryTester(lsst.utils.tests.MemoryTestCase):
296 pass
299def setup_module(module):
300 lsst.utils.tests.init()
303if __name__ == "__main__": 303 ↛ 305line 303 didn't jump to line 305, because the condition on line 303 was never true
305 lsst.utils.tests.init()
306 unittest.main()