Coverage for tests/test_matchPessimisticB.py: 16%

152 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-07 10:35 +0000

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/>. 

21 

22import math 

23import os 

24import unittest 

25 

26import numpy as np 

27 

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 

35 

36 

37class TestMatchPessimisticB(unittest.TestCase): 

38 

39 def setUp(self): 

40 np.random.seed(12345) 

41 

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) 

49 

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 

54 

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

56 self.tolArcsec = .4 

57 self.tolPixel = .1 

58 

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 

63 

64 def tearDown(self): 

65 del self.config 

66 del self.MatchPessimisticB 

67 del self.wcs 

68 del self.distortedWcs 

69 

70 def testLinearXDistort(self): 

71 self.singleTestInstance(self.filename, genDistortedImage.linearXDistort) 

72 

73 def testLinearYDistort(self): 

74 self.singleTestInstance(self.filename, genDistortedImage.linearYDistort) 

75 

76 def testQuadraticDistort(self): 

77 self.singleTestInstance(self.filename, genDistortedImage.quadraticDistort) 

78 

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. 

82 

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) 

89 

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 

95 

96 self.singleTestInstance(self.filename, applyDistortion) 

97 

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

99 sourceCat = self.loadSourceCatalog(self.filename) 

100 refCat = self.computePosRefCatalog(sourceCat) 

101 

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) 

106 

107 distortedCat = genDistortedImage.distortList(sourceSelection.sourceCat, distortFunc) 

108 

109 if doPlot: 

110 import matplotlib.pyplot as plt 

111 

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] 

115 

116 def plot(catalog, symbol): 

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

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

119 

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. 

125 

126 plt.show() 

127 

128 sourceCat = distortedCat 

129 

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) 

142 

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 

147 

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) 

154 

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

163 

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

165 

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) 

172 

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) 

177 

178 distortedCat = genDistortedImage.distortList(sourceSelection.sourceCat, 

179 genDistortedImage.linearXDistort) 

180 

181 sourceCat = distortedCat 

182 

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 ) 

190 

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 ) 

202 

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 ) 

211 

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) 

220 

221 def testReferenceFilter(self): 

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

223 sourceCat = self.loadSourceCatalog(self.filename) 

224 refCat = self.computePosRefCatalog(sourceCat) 

225 

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) 

230 

231 distortedCat = genDistortedImage.distortList(sourceSelection.sourceCat, 

232 genDistortedImage.linearXDistort) 

233 

234 matchPessConfig = measAstrom.MatchPessimisticBTask.ConfigClass() 

235 matchPessConfig.maxRefObjects = 150 

236 matchPessConfig.minMatchDistPixels = 5.0 

237 

238 matchPess = measAstrom.MatchPessimisticBTask(config=matchPessConfig) 

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

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

241 

242 matchRes = matchPess.matchObjectsToSources( 

243 refCat=refCat, 

244 sourceCat=distortedCat, 

245 wcs=self.distortedWcs, 

246 sourceFluxField='slot_ApFlux_instFlux', 

247 refFluxField="r_flux", 

248 ) 

249 

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

251 

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 

268 

269 def loadSourceCatalog(self, filename): 

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

271 SourceSet of points 

272 

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

279 

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) 

287 

288 # Set catalog coord 

289 for src in sourceCat: 

290 src.updateCoord(self.wcs) 

291 return sourceCat 

292 

293 

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

295 pass 

296 

297 

298def setup_module(module): 

299 lsst.utils.tests.init() 

300 

301 

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

303 

304 lsst.utils.tests.init() 

305 unittest.main()