Coverage for tests/test_matchOptimisticB.py: 20%

134 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-30 10:46 +0000

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# 

22 

23import math 

24import os 

25import unittest 

26import pickle 

27 

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 LoadReferenceObjectsTask 

34import lsst.meas.astrom.sip.genDistortedImage as distort 

35import lsst.meas.astrom as measAstrom 

36import lsst.meas.astrom.matchOptimisticB as matchOptimisticB 

37 

38 

39class TestMatchOptimisticB(unittest.TestCase): 

40 

41 def setUp(self): 

42 

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 

49 

50 self.filename = os.path.join(os.path.dirname(__file__), "cat.xy.fits") 

51 self.tolArcsec = .4 

52 self.tolPixel = .1 

53 

54 def tearDown(self): 

55 del self.config 

56 del self.matchOptimisticB 

57 del self.wcs 

58 del self.distortedWcs 

59 

60 def testLinearXDistort(self): 

61 self.singleTestInstance(self.filename, distort.linearXDistort) 

62 

63 def testLinearYDistort(self): 

64 self.singleTestInstance(self.filename, distort.linearYDistort) 

65 

66 def testQuadraticDistort(self): 

67 self.singleTestInstance(self.filename, distort.quadraticDistort) 

68 

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) 

77 

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 

83 

84 self.singleTestInstance(self.filename, applyDistortion) 

85 

86 def singleTestInstance(self, filename, distortFunc, doPlot=False): 

87 sourceCat = self.loadSourceCatalog(self.filename) 

88 refCat = self.computePosRefCatalog(sourceCat) 

89 

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) 

96 

97 distortedCat = distort.distortList(sourceSelection.sourceCat, distortFunc) 

98 

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] 

104 

105 def plot(catalog, symbol): 

106 plt.plot([ss.getX() for ss in catalog], [ss.getY() for ss in catalog], symbol) 

107 

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() 

115 

116 sourceCat = distortedCat 

117 

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) 

129 

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) 

140 

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)) 

147 

148 self.assertLess(maxDistErr.asArcseconds(), 1e-7) 

149 

150 def computePosRefCatalog(self, sourceCat): 

151 """Generate a position reference catalog from a source catalog 

152 """ 

153 minimalPosRefSchema = LoadReferenceObjectsTask.makeMinimalSchema(filterNameList=["r"], 

154 addCentroid=True) 

155 refCat = afwTable.SimpleCatalog(minimalPosRefSchema) 

156 for source in sourceCat: 

157 refObj = refCat.addNew() 

158 refObj.setCoord(source.getCoord()) 

159 refObj.set("centroid_x", source.getX()) 

160 refObj.set("centroid_y", source.getY()) 

161 refObj.set("hasCentroid", True) 

162 refObj.set("r_flux", source.get("slot_ApFlux_instFlux")) 

163 refObj.set("r_fluxErr", source.get("slot_ApFlux_instFluxErr")) 

164 refObj.setId(source.getId()) 

165 return refCat 

166 

167 def loadSourceCatalog(self, filename): 

168 """Load a list of xy points from a file, set coord, and return a SourceSet of points 

169 """ 

170 sourceCat = afwTable.SourceCatalog.readFits(filename) 

171 aliasMap = sourceCat.schema.getAliasMap() 

172 aliasMap.set("slot_ApFlux", "base_PsfFlux") 

173 instFluxKey = sourceCat.schema["slot_ApFlux_instFlux"].asKey() 

174 instFluxErrKey = sourceCat.schema["slot_ApFlux_instFluxErr"].asKey() 

175 

176 # print("schema=", sourceCat.schema) 

177 

178 # Source x,y positions are ~ (500,1500) x (500,1500) 

179 centroidKey = sourceCat.table.getCentroidSlot().getMeasKey() 

180 for src in sourceCat: 

181 adjCentroid = src.get(centroidKey) - lsst.geom.Extent2D(500, 500) 

182 src.set(centroidKey, adjCentroid) 

183 src.set(instFluxKey, 1000) 

184 src.set(instFluxErrKey, 1) 

185 

186 # Set catalog coord 

187 for src in sourceCat: 

188 src.updateCoord(self.wcs) 

189 return sourceCat 

190 

191 def testArgumentErrors(self): 

192 """Test argument sanity checking in matchOptimisticB 

193 """ 

194 matchControl = matchOptimisticB.MatchOptimisticBControl() 

195 

196 sourceCat = self.loadSourceCatalog(self.filename) 

197 emptySourceCat = afwTable.SourceCatalog(sourceCat.schema) 

198 

199 refCat = self.computePosRefCatalog(sourceCat) 

200 emptyRefCat = afwTable.SimpleCatalog(refCat.schema) 

201 

202 with self.assertRaises(pexExcept.InvalidParameterError): 

203 matchOptimisticB.matchOptimisticB( 

204 emptyRefCat, 

205 sourceCat, 

206 matchControl, 

207 self.wcs, 

208 0, 

209 ) 

210 with self.assertRaises(pexExcept.InvalidParameterError): 

211 matchOptimisticB.matchOptimisticB( 

212 refCat, 

213 emptySourceCat, 

214 matchControl, 

215 self.wcs, 

216 0, 

217 ) 

218 with self.assertRaises(pexExcept.InvalidParameterError): 

219 matchOptimisticB.matchOptimisticB( 

220 refCat, 

221 sourceCat, 

222 matchControl, 

223 self.wcs, 

224 len(refCat), 

225 ) 

226 with self.assertRaises(pexExcept.InvalidParameterError): 

227 matchOptimisticB.matchOptimisticB( 

228 refCat, 

229 sourceCat, 

230 matchControl, 

231 self.wcs, 

232 -1, 

233 ) 

234 

235 def testConfigPickle(self): 

236 """Test that we can pickle the Config 

237 

238 This is required for use in singleFrameDriver. 

239 See DM-18314. 

240 """ 

241 config = pickle.loads(pickle.dumps(self.config)) 

242 self.assertEqual(config, self.config) 

243 

244 

245class MemoryTester(lsst.utils.tests.MemoryTestCase): 

246 pass 

247 

248 

249def setup_module(module): 

250 lsst.utils.tests.init() 

251 

252 

253if __name__ == "__main__": 253 ↛ 255line 253 didn't jump to line 255, because the condition on line 253 was never true

254 

255 lsst.utils.tests.init() 

256 unittest.main()