Coverage for tests/test_matchOptimisticB.py: 18%

134 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-23 02:30 -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# 

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 convertReferenceCatalog 

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 = convertReferenceCatalog._makeSchema(filterNameList=["r"], addCentroid=True) 

154 refCat = afwTable.SimpleCatalog(minimalPosRefSchema) 

155 for source in sourceCat: 

156 refObj = refCat.addNew() 

157 refObj.setCoord(source.getCoord()) 

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

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

160 refObj.set("hasCentroid", True) 

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

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

163 refObj.setId(source.getId()) 

164 return refCat 

165 

166 def loadSourceCatalog(self, filename): 

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

168 """ 

169 sourceCat = afwTable.SourceCatalog.readFits(filename) 

170 aliasMap = sourceCat.schema.getAliasMap() 

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

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

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

174 

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

176 

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

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

179 for src in sourceCat: 

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

181 src.set(centroidKey, adjCentroid) 

182 src.set(instFluxKey, 1000) 

183 src.set(instFluxErrKey, 1) 

184 

185 # Set catalog coord 

186 for src in sourceCat: 

187 src.updateCoord(self.wcs) 

188 return sourceCat 

189 

190 def testArgumentErrors(self): 

191 """Test argument sanity checking in matchOptimisticB 

192 """ 

193 matchControl = matchOptimisticB.MatchOptimisticBControl() 

194 

195 sourceCat = self.loadSourceCatalog(self.filename) 

196 emptySourceCat = afwTable.SourceCatalog(sourceCat.schema) 

197 

198 refCat = self.computePosRefCatalog(sourceCat) 

199 emptyRefCat = afwTable.SimpleCatalog(refCat.schema) 

200 

201 with self.assertRaises(pexExcept.InvalidParameterError): 

202 matchOptimisticB.matchOptimisticB( 

203 emptyRefCat, 

204 sourceCat, 

205 matchControl, 

206 self.wcs, 

207 0, 

208 ) 

209 with self.assertRaises(pexExcept.InvalidParameterError): 

210 matchOptimisticB.matchOptimisticB( 

211 refCat, 

212 emptySourceCat, 

213 matchControl, 

214 self.wcs, 

215 0, 

216 ) 

217 with self.assertRaises(pexExcept.InvalidParameterError): 

218 matchOptimisticB.matchOptimisticB( 

219 refCat, 

220 sourceCat, 

221 matchControl, 

222 self.wcs, 

223 len(refCat), 

224 ) 

225 with self.assertRaises(pexExcept.InvalidParameterError): 

226 matchOptimisticB.matchOptimisticB( 

227 refCat, 

228 sourceCat, 

229 matchControl, 

230 self.wcs, 

231 -1, 

232 ) 

233 

234 def testConfigPickle(self): 

235 """Test that we can pickle the Config 

236 

237 This is required for use in singleFrameDriver. 

238 See DM-18314. 

239 """ 

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

241 self.assertEqual(config, self.config) 

242 

243 

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

245 pass 

246 

247 

248def setup_module(module): 

249 lsst.utils.tests.init() 

250 

251 

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

253 

254 lsst.utils.tests.init() 

255 unittest.main()