Coverage for tests/test_matchPessimisticB.py: 18%
152 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-30 10:46 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-30 10:46 +0000
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 LoadReferenceObjectsTask
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 = LoadReferenceObjectsTask.makeMinimalSchema(filterNameList=["r"],
257 addCentroid=True)
258 refCat = afwTable.SimpleCatalog(minimalPosRefSchema)
259 refCat.reserve(len(sourceCat))
260 for source in sourceCat:
261 refObj = refCat.addNew()
262 refObj.setCoord(source.getCoord())
263 refObj.set("centroid_x", source.getX())
264 refObj.set("centroid_y", source.getY())
265 refObj.set("hasCentroid", True)
266 refObj.set("r_flux", np.random.uniform(1, 10000))
267 refObj.set("r_fluxErr", source.get("slot_ApFlux_instFluxErr"))
268 refObj.setId(source.getId())
269 return refCat
271 def loadSourceCatalog(self, filename):
272 """Load a list of xy points from a file, set coord, and return a
273 SourceSet of points
275 """
276 sourceCat = afwTable.SourceCatalog.readFits(filename)
277 aliasMap = sourceCat.schema.getAliasMap()
278 aliasMap.set("slot_ApFlux", "base_PsfFlux")
279 instFluxKey = sourceCat.schema["slot_ApFlux_instFlux"].asKey()
280 instFluxErrKey = sourceCat.schema["slot_ApFlux_instFluxErr"].asKey()
282 # Source x,y positions are ~ (500,1500) x (500,1500)
283 centroidKey = sourceCat.table.getCentroidSlot().getMeasKey()
284 for src in sourceCat:
285 adjCentroid = src.get(centroidKey) - lsst.geom.Extent2D(500, 500)
286 src.set(centroidKey, adjCentroid)
287 src.set(instFluxKey, 1000)
288 src.set(instFluxErrKey, 1)
290 # Set catalog coord
291 for src in sourceCat:
292 src.updateCoord(self.wcs)
293 return sourceCat
296class MemoryTester(lsst.utils.tests.MemoryTestCase):
297 pass
300def setup_module(module):
301 lsst.utils.tests.init()
304if __name__ == "__main__": 304 ↛ 306line 304 didn't jump to line 306, because the condition on line 304 was never true
306 lsst.utils.tests.init()
307 unittest.main()