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