Coverage for tests/test_matchPessimisticB.py: 16%

152 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-12 11:09 +0000

1 

2 

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 

27 

28import numpy as np 

29 

30import lsst.geom 

31import lsst.afw.geom as afwGeom 

32import lsst.afw.table as afwTable 

33import lsst.utils.tests 

34from lsst.meas.algorithms import convertReferenceCatalog 

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

36import lsst.meas.astrom as measAstrom 

37 

38 

39class TestMatchPessimisticB(unittest.TestCase): 

40 

41 def setUp(self): 

42 

43 np.random.seed(12345) 

44 

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) 

52 

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 

57 

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

59 self.tolArcsec = .4 

60 self.tolPixel = .1 

61 

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 

66 

67 def tearDown(self): 

68 del self.config 

69 del self.MatchPessimisticB 

70 del self.wcs 

71 del self.distortedWcs 

72 

73 def testLinearXDistort(self): 

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

75 

76 def testLinearYDistort(self): 

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

78 

79 def testQuadraticDistort(self): 

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

81 

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. 

85 

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) 

92 

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 

98 

99 self.singleTestInstance(self.filename, applyDistortion) 

100 

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

102 sourceCat = self.loadSourceCatalog(self.filename) 

103 refCat = self.computePosRefCatalog(sourceCat) 

104 

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) 

109 

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

111 

112 if doPlot: 

113 import matplotlib.pyplot as plt 

114 

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] 

118 

119 def plot(catalog, symbol): 

120 plt.plot([ss.getX() for ss in catalog], 

121 [ss.getY() for ss in catalog], symbol) 

122 

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. 

128 

129 plt.show() 

130 

131 sourceCat = distortedCat 

132 

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) 

145 

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 

150 

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) 

157 

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

166 

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

168 

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) 

175 

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) 

180 

181 distortedCat = distort.distortList(sourceSelection.sourceCat, distort.linearXDistort) 

182 

183 sourceCat = distortedCat 

184 

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 ) 

192 

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 ) 

204 

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 ) 

213 

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) 

222 

223 def testReferenceFilter(self): 

224 """Test sub-selecting reference objects by flux.""" 

225 sourceCat = self.loadSourceCatalog(self.filename) 

226 refCat = self.computePosRefCatalog(sourceCat) 

227 

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) 

232 

233 distortedCat = distort.distortList(sourceSelection.sourceCat, distort.linearXDistort) 

234 

235 matchPessConfig = measAstrom.MatchPessimisticBTask.ConfigClass() 

236 matchPessConfig.maxRefObjects = 150 

237 matchPessConfig.minMatchDistPixels = 5.0 

238 

239 matchPess = measAstrom.MatchPessimisticBTask(config=matchPessConfig) 

240 trimmedRefCat = matchPess._filterRefCat(refCat, 'r_flux') 

241 self.assertEqual(len(trimmedRefCat), matchPessConfig.maxRefObjects) 

242 

243 matchRes = matchPess.matchObjectsToSources( 

244 refCat=refCat, 

245 sourceCat=distortedCat, 

246 wcs=self.distortedWcs, 

247 sourceFluxField='slot_ApFlux_instFlux', 

248 refFluxField="r_flux", 

249 ) 

250 

251 self.assertEqual(len(matchRes.matches), matchPessConfig.maxRefObjects - 3) 

252 

253 def computePosRefCatalog(self, sourceCat): 

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

255 """ 

256 minimalPosRefSchema = convertReferenceCatalog._makeSchema(filterNameList=["r"], addCentroid=True) 

257 refCat = afwTable.SimpleCatalog(minimalPosRefSchema) 

258 refCat.reserve(len(sourceCat)) 

259 for source in sourceCat: 

260 refObj = refCat.addNew() 

261 refObj.setCoord(source.getCoord()) 

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

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

264 refObj.set("hasCentroid", True) 

265 refObj.set("r_flux", np.random.uniform(1, 10000)) 

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

267 refObj.setId(source.getId()) 

268 return refCat 

269 

270 def loadSourceCatalog(self, filename): 

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

272 SourceSet of points 

273 

274 """ 

275 sourceCat = afwTable.SourceCatalog.readFits(filename) 

276 aliasMap = sourceCat.schema.getAliasMap() 

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

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

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

280 

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

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

283 for src in sourceCat: 

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

285 src.set(centroidKey, adjCentroid) 

286 src.set(instFluxKey, 1000) 

287 src.set(instFluxErrKey, 1) 

288 

289 # Set catalog coord 

290 for src in sourceCat: 

291 src.updateCoord(self.wcs) 

292 return sourceCat 

293 

294 

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

296 pass 

297 

298 

299def setup_module(module): 

300 lsst.utils.tests.init() 

301 

302 

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

304 

305 lsst.utils.tests.init() 

306 unittest.main()