Coverage for tests/test_matchPessimisticB.py: 16%
152 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:58 -0700
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 03:58 -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 os
24import unittest
26import numpy as np
28import lsst.geom
29import lsst.afw.geom as afwGeom
30import lsst.afw.table as afwTable
31import lsst.utils.tests
32from lsst.meas.algorithms import convertReferenceCatalog
33from lsst.meas.astrom.sip import genDistortedImage
34import lsst.meas.astrom as measAstrom
37class TestMatchPessimisticB(unittest.TestCase):
39 def setUp(self):
40 np.random.seed(12345)
42 self.config = measAstrom.MatchPessimisticBTask.ConfigClass()
43 # Value below is to assure all matches are selected. The
44 # original test is set for a 3 arcsecond max match distance
45 # using matchOptimisticB.
46 self.config.minMatchDistPixels = 2.0
47 self.MatchPessimisticB = measAstrom.MatchPessimisticBTask(
48 config=self.config)
50 self.wcs = afwGeom.makeSkyWcs(crpix=lsst.geom.Point2D(791.4, 559.7),
51 crval=lsst.geom.SpherePoint(36.930640, -4.939560, lsst.geom.degrees),
52 cdMatrix=afwGeom.makeCdMatrix(scale=5.17e-5*lsst.geom.degrees))
53 self.distortedWcs = self.wcs
55 self.filename = os.path.join(os.path.dirname(__file__), "cat.xy.fits")
56 self.tolArcsec = .4
57 self.tolPixel = .1
59 # 3 of the objects are removed by the source selector and are used in
60 # matching hence the 183 number vs the total of 186. This is also why
61 # these three objects are missing in the testReferenceFilter test.
62 self.expectedMatches = 183
64 def tearDown(self):
65 del self.config
66 del self.MatchPessimisticB
67 del self.wcs
68 del self.distortedWcs
70 def testLinearXDistort(self):
71 self.singleTestInstance(self.filename, genDistortedImage.linearXDistort)
73 def testLinearYDistort(self):
74 self.singleTestInstance(self.filename, genDistortedImage.linearYDistort)
76 def testQuadraticDistort(self):
77 self.singleTestInstance(self.filename, genDistortedImage.quadraticDistort)
79 def testLargeDistortion(self):
80 # This transform is about as extreme as I can get:
81 # using 0.0005 in the last value appears to produce numerical issues.
83 # It produces a maximum deviation of 459 pixels, which should be
84 # sufficient.
85 pixelsToTanPixels = afwGeom.makeRadialTransform([0.0, 1.1, 0.0004])
86 self.distortedWcs = afwGeom.makeModifiedWcs(pixelTransform=pixelsToTanPixels,
87 wcs=self.wcs,
88 modifyActualPixels=False)
90 def applyDistortion(src):
91 out = src.table.copyRecord(src)
92 out.set(out.table.getCentroidSlot().getMeasKey(),
93 pixelsToTanPixels.applyInverse(src.getCentroid()))
94 return out
96 self.singleTestInstance(self.filename, applyDistortion)
98 def singleTestInstance(self, filename, distortFunc, doPlot=False):
99 sourceCat = self.loadSourceCatalog(self.filename)
100 refCat = self.computePosRefCatalog(sourceCat)
102 # Apply source selector to sourceCat, using the astrometry config defaults
103 tempConfig = measAstrom.AstrometryTask.ConfigClass()
104 tempSolver = measAstrom.AstrometryTask(config=tempConfig, refObjLoader=None)
105 sourceSelection = tempSolver.sourceSelector.run(sourceCat)
107 distortedCat = genDistortedImage.distortList(sourceSelection.sourceCat, distortFunc)
109 if doPlot:
110 import matplotlib.pyplot as plt
112 undistorted = [self.wcs.skyToPixel(self.distortedWcs.pixelToSky(ss.getCentroid()))
113 for ss in distortedCat]
114 refs = [self.wcs.skyToPixel(ss.getCoord()) for ss in refCat]
116 def plot(catalog, symbol):
117 plt.plot([ss.getX() for ss in catalog],
118 [ss.getY() for ss in catalog], symbol)
120 plot(distortedCat, 'b+') # Distorted positions: blue +
121 plot(undistorted, 'g+') # Undistorted positions: green +
122 plot(refs, 'rx') # Reference catalog: red x
123 # The green + should overlap with the red x, because that's how
124 # MatchPessimisticB does it.
126 plt.show()
128 sourceCat = distortedCat
130 matchRes = self.MatchPessimisticB.matchObjectsToSources(
131 refCat=refCat,
132 sourceCat=sourceCat,
133 wcs=self.distortedWcs,
134 sourceFluxField='slot_ApFlux_instFlux',
135 refFluxField="r_flux",
136 )
137 matches = matchRes.matches
138 if doPlot:
139 measAstrom.plotAstrometry(matches=matches, refCat=refCat,
140 sourceCat=sourceCat)
141 self.assertEqual(len(matches), self.expectedMatches)
143 refCoordKey = afwTable.CoordKey(refCat.schema["coord"])
144 srcCoordKey = afwTable.CoordKey(sourceCat.schema["coord"])
145 refCentroidKey = afwTable.Point2DKey(refCat.getSchema()["centroid"])
146 maxDistErr = 0*lsst.geom.radians
148 for refObj, source, distRad in matches:
149 sourceCoord = source.get(srcCoordKey)
150 refCoord = refObj.get(refCoordKey)
151 predDist = sourceCoord.separation(refCoord)
152 distErr = abs(predDist - distRad*lsst.geom.radians)
153 maxDistErr = max(distErr, maxDistErr)
155 if refObj.getId() != source.getId():
156 refCentroid = refObj.get(refCentroidKey)
157 sourceCentroid = source.getCentroid()
158 radius = math.hypot(*(refCentroid - sourceCentroid))
159 self.fail(
160 "ID mismatch: %s at %s != %s at %s; error = %0.1f pix" %
161 (refObj.getId(), refCentroid, source.getId(),
162 sourceCentroid, radius))
164 self.assertLess(maxDistErr.asArcseconds(), 1e-7)
166 def testPassingMatcherState(self):
167 """Test that results of the matcher can be propagated to to in
168 subsequent iterations.
169 """
170 sourceCat = self.loadSourceCatalog(self.filename)
171 refCat = self.computePosRefCatalog(sourceCat)
173 # Apply source selector to sourceCat, using the astrometry config defaults
174 tempConfig = measAstrom.AstrometryTask.ConfigClass()
175 tempSolver = measAstrom.AstrometryTask(config=tempConfig, refObjLoader=None)
176 sourceSelection = tempSolver.sourceSelector.run(sourceCat)
178 distortedCat = genDistortedImage.distortList(sourceSelection.sourceCat,
179 genDistortedImage.linearXDistort)
181 sourceCat = distortedCat
183 matchRes = self.MatchPessimisticB.matchObjectsToSources(
184 refCat=refCat,
185 sourceCat=sourceCat,
186 wcs=self.distortedWcs,
187 sourceFluxField='slot_ApFlux_instFlux',
188 refFluxField="r_flux",
189 )
191 maxShift = matchRes.matchTolerance.maxShift * 300
192 # Force the matcher to use a different pattern thatn the previous
193 # "iteration".
194 matchTol = measAstrom.MatchTolerancePessimistic(
195 maxMatchDist=matchRes.matchTolerance.maxMatchDist,
196 autoMaxMatchDist=matchRes.matchTolerance.autoMaxMatchDist,
197 maxShift=maxShift,
198 lastMatchedPattern=0,
199 failedPatternList=[0],
200 PPMbObj=matchRes.matchTolerance.PPMbObj,
201 )
203 matchRes = self.MatchPessimisticB.matchObjectsToSources(
204 refCat=refCat,
205 sourceCat=sourceCat,
206 wcs=self.distortedWcs,
207 sourceFluxField='slot_ApFlux_instFlux',
208 refFluxField="r_flux",
209 matchTolerance=matchTol,
210 )
212 self.assertEqual(len(matchRes.matches), self.expectedMatches)
213 self.assertLess(matchRes.matchTolerance.maxShift, maxShift)
214 self.assertEqual(matchRes.matchTolerance.lastMatchedPattern, 1)
215 self.assertIsNotNone(matchRes.matchTolerance.maxMatchDist)
216 self.assertIsNotNone(matchRes.matchTolerance.autoMaxMatchDist)
217 self.assertIsNotNone(matchRes.matchTolerance.lastMatchedPattern)
218 self.assertIsNotNone(matchRes.matchTolerance.failedPatternList)
219 self.assertIsNotNone(matchRes.matchTolerance.PPMbObj)
221 def testReferenceFilter(self):
222 """Test sub-selecting reference objects by flux."""
223 sourceCat = self.loadSourceCatalog(self.filename)
224 refCat = self.computePosRefCatalog(sourceCat)
226 # Apply source selector to sourceCat, using the astrometry config defaults
227 tempConfig = measAstrom.AstrometryTask.ConfigClass()
228 tempSolver = measAstrom.AstrometryTask(config=tempConfig, refObjLoader=None)
229 sourceSelection = tempSolver.sourceSelector.run(sourceCat)
231 distortedCat = genDistortedImage.distortList(sourceSelection.sourceCat,
232 genDistortedImage.linearXDistort)
234 matchPessConfig = measAstrom.MatchPessimisticBTask.ConfigClass()
235 matchPessConfig.maxRefObjects = 150
236 matchPessConfig.minMatchDistPixels = 5.0
238 matchPess = measAstrom.MatchPessimisticBTask(config=matchPessConfig)
239 trimmedRefCat = matchPess._filterRefCat(refCat, 'r_flux')
240 self.assertEqual(len(trimmedRefCat), matchPessConfig.maxRefObjects)
242 matchRes = matchPess.matchObjectsToSources(
243 refCat=refCat,
244 sourceCat=distortedCat,
245 wcs=self.distortedWcs,
246 sourceFluxField='slot_ApFlux_instFlux',
247 refFluxField="r_flux",
248 )
250 self.assertEqual(len(matchRes.matches), matchPessConfig.maxRefObjects - 3)
252 def computePosRefCatalog(self, sourceCat):
253 """Generate a position reference catalog from a source catalog
254 """
255 minimalPosRefSchema = convertReferenceCatalog._makeSchema(filterNameList=["r"], addCentroid=True)
256 refCat = afwTable.SimpleCatalog(minimalPosRefSchema)
257 refCat.reserve(len(sourceCat))
258 for source in sourceCat:
259 refObj = refCat.addNew()
260 refObj.setCoord(source.getCoord())
261 refObj.set("centroid_x", source.getX())
262 refObj.set("centroid_y", source.getY())
263 refObj.set("hasCentroid", True)
264 refObj.set("r_flux", np.random.uniform(1, 10000))
265 refObj.set("r_fluxErr", source.get("slot_ApFlux_instFluxErr"))
266 refObj.setId(source.getId())
267 return refCat
269 def loadSourceCatalog(self, filename):
270 """Load a list of xy points from a file, set coord, and return a
271 SourceSet of points
273 """
274 sourceCat = afwTable.SourceCatalog.readFits(filename)
275 aliasMap = sourceCat.schema.getAliasMap()
276 aliasMap.set("slot_ApFlux", "base_PsfFlux")
277 instFluxKey = sourceCat.schema["slot_ApFlux_instFlux"].asKey()
278 instFluxErrKey = sourceCat.schema["slot_ApFlux_instFluxErr"].asKey()
280 # Source x,y positions are ~ (500,1500) x (500,1500)
281 centroidKey = sourceCat.table.getCentroidSlot().getMeasKey()
282 for src in sourceCat:
283 adjCentroid = src.get(centroidKey) - lsst.geom.Extent2D(500, 500)
284 src.set(centroidKey, adjCentroid)
285 src.set(instFluxKey, 1000)
286 src.set(instFluxErrKey, 1)
288 # Set catalog coord
289 for src in sourceCat:
290 src.updateCoord(self.wcs)
291 return sourceCat
294class MemoryTester(lsst.utils.tests.MemoryTestCase):
295 pass
298def setup_module(module):
299 lsst.utils.tests.init()
302if __name__ == "__main__": 302 ↛ 304line 302 didn't jump to line 304, because the condition on line 302 was never true
304 lsst.utils.tests.init()
305 unittest.main()