Coverage for tests/test_fitAffineWcsTask.py: 21%

136 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-12 09:40 +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 unittest 

24 

25import numpy as np 

26 

27import lsst.pipe.base 

28import lsst.utils.tests 

29import lsst.geom 

30import lsst.afw.geom as afwGeom 

31import lsst.afw.table as afwTable 

32from lsst.meas.algorithms import convertReferenceCatalog 

33from lsst.meas.base import SingleFrameMeasurementTask 

34from lsst.meas.astrom import FitAffineWcsTask, TransformedSkyWcsMaker 

35 

36 

37class BaseTestCase: 

38 

39 """A test case for FitAffineWcsTask. 

40 

41 Use involves setting one class attribute: 

42 * MatchClass: match class, e.g. ReferenceMatch or SourceMatch 

43 """ 

44 MatchClass = None 

45 

46 def setUp(self): 

47 crval = lsst.geom.SpherePoint(44, 45, lsst.geom.degrees) 

48 crpix = lsst.geom.Point2D(15000, 4000) 

49 

50 scale = 1 * lsst.geom.arcseconds 

51 cdMatrix = afwGeom.makeCdMatrix(scale=scale, flipX=True) 

52 self.tanWcs = afwGeom.makeSkyWcs(crpix=crpix, crval=crval, cdMatrix=cdMatrix) 

53 self.loadData() 

54 

55 def loadData(self, rangePix=3000, numPoints=25): 

56 """Load catalogs and make the match list 

57 

58 This is a separate function so data can be reloaded if fitting more than once 

59 (each time a WCS is fit it may update the source catalog, reference catalog and match list) 

60 """ 

61 refSchema = convertReferenceCatalog._makeSchema(filterNameList=["r"], addIsPhotometric=True, 

62 addCentroid=True) 

63 self.refCat = afwTable.SimpleCatalog(refSchema) 

64 srcSchema = afwTable.SourceTable.makeMinimalSchema() 

65 SingleFrameMeasurementTask(schema=srcSchema) 

66 self.srcCoordKey = afwTable.CoordKey(srcSchema["coord"]) 

67 self.srcCentroidKey = afwTable.Point2DKey(srcSchema["slot_Centroid"]) 

68 self.srcCentroidKey_xErr = srcSchema["slot_Centroid_xErr"].asKey() 

69 self.srcCentroidKey_yErr = srcSchema["slot_Centroid_yErr"].asKey() 

70 self.sourceCat = afwTable.SourceCatalog(srcSchema) 

71 

72 self.matches = [] 

73 

74 for i in np.linspace(0., rangePix, numPoints): 

75 for j in np.linspace(0., rangePix, numPoints): 

76 src = self.sourceCat.addNew() 

77 refObj = self.refCat.addNew() 

78 

79 src.set(self.srcCentroidKey, lsst.geom.Point2D(i, j)) 

80 src.set(self.srcCentroidKey_xErr, 0.1) 

81 src.set(self.srcCentroidKey_yErr, 0.1) 

82 

83 c = self.tanWcs.pixelToSky(i, j) 

84 refObj.setCoord(c) 

85 

86 self.matches.append(self.MatchClass(refObj, src, 0.0)) 

87 

88 def tearDown(self): 

89 del self.refCat 

90 del self.sourceCat 

91 del self.matches 

92 del self.tanWcs 

93 

94 def checkResults(self, fitRes, catsUpdated): 

95 """Check results. 

96 """ 

97 self.assertLess(fitRes.scatterOnSky.asArcseconds(), 0.001) 

98 affineWcs = fitRes.wcs 

99 maxAngSep = 0*lsst.geom.radians 

100 maxPixSep = 0 

101 refCoordKey = afwTable.CoordKey(self.refCat.schema["coord"]) 

102 if catsUpdated: 

103 refCentroidKey = afwTable.Point2DKey(self.refCat.schema["centroid"]) 

104 maxDistErr = 0*lsst.geom.radians 

105 for refObj, src, distRad in self.matches: 

106 srcPixPos = src.get(self.srcCentroidKey) 

107 refCoord = refObj.get(refCoordKey) 

108 if catsUpdated: 

109 refPixPos = refObj.get(refCentroidKey) 

110 srcCoord = src.get(self.srcCoordKey) 

111 else: 

112 refPixPos = affineWcs.skyToPixel(refCoord) 

113 srcCoord = affineWcs.pixelToSky(srcPixPos) 

114 

115 angSep = refCoord.separation(srcCoord) 

116 dist = distRad*lsst.geom.radians 

117 distErr = abs(dist - angSep) 

118 maxDistErr = max(maxDistErr, distErr) 

119 maxAngSep = max(maxAngSep, angSep) 

120 

121 pixSep = math.hypot(*(srcPixPos - refPixPos)) 

122 maxPixSep = max(maxPixSep, pixSep) 

123 

124 print("max angular separation = %0.4f arcsec" % (maxAngSep.asArcseconds(),)) 

125 print("max pixel separation = %0.3f" % (maxPixSep,)) 

126 self.assertLess(maxAngSep.asArcseconds(), 0.001) 

127 self.assertLess(maxPixSep, 0.005) 

128 if catsUpdated: 

129 allowedDistErr = 1e-7 

130 else: 

131 allowedDistErr = 0.001 

132 self.assertLess(maxDistErr.asArcseconds(), allowedDistErr, 

133 "Computed distance in match list is off by %s arcsec" % (maxDistErr.asArcseconds(),)) 

134 

135 def doTest(self, name, func): 

136 """Apply func(x, y) to each source in self.sourceCat, then fit and 

137 check the resulting WCS. 

138 """ 

139 bbox = lsst.geom.Box2I() 

140 for refObj, src, d in self.matches: 

141 origPos = src.get(self.srcCentroidKey) 

142 x, y = func(*origPos) 

143 distortedPos = lsst.geom.Point2D(x, y) 

144 src.set(self.srcCentroidKey, distortedPos) 

145 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(distortedPos))) 

146 

147 fitter = FitAffineWcsTask() 

148 fitRes = fitter.fitWcs( 

149 matches=self.matches, 

150 initWcs=self.tanWcs, 

151 bbox=bbox, 

152 refCat=self.refCat, 

153 sourceCat=self.sourceCat, 

154 ) 

155 

156 self.checkResults(fitRes, catsUpdated=True) 

157 

158 def doTestAffine(self, name, offset, matrix): 

159 """Apply func(x, y) to each source in self.sourceCat, then fit and 

160 check the resulting WCS. 

161 """ 

162 wcsMaker = TransformedSkyWcsMaker(self.tanWcs) 

163 

164 newWcs = wcsMaker.makeWcs(offset, matrix) 

165 bbox = lsst.geom.Box2I() 

166 for refObj, src, d in self.matches: 

167 origPos = src.get(self.srcCentroidKey) 

168 newCoord = newWcs.pixelToSky(origPos) 

169 src.setCoord(newCoord) 

170 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(origPos))) 

171 

172 fitter = FitAffineWcsTask() 

173 fitRes = fitter.fitWcs( 

174 matches=self.matches, 

175 initWcs=newWcs, 

176 bbox=bbox, 

177 refCat=self.refCat, 

178 sourceCat=self.sourceCat, 

179 ) 

180 

181 self.checkResults(fitRes, catsUpdated=True) 

182 

183 def doTestAffineReverse(self, name, offset, matrix): 

184 """Apply func(x, y) to each source in self.sourceCat, then fit and 

185 check the resulting WCS. 

186 """ 

187 wcsMaker = TransformedSkyWcsMaker(self.tanWcs) 

188 

189 newWcs = wcsMaker.makeWcs(offset, matrix) 

190 bbox = lsst.geom.Box2I() 

191 for refObj, src, d in self.matches: 

192 origCoord = src.get(self.srcCoordKey) 

193 newCentroid = newWcs.skyToPixel(origCoord) 

194 src.set(self.srcCentroidKey, newCentroid) 

195 bbox.include(lsst.geom.Point2I(lsst.geom.Point2I(newCentroid))) 

196 

197 fitter = FitAffineWcsTask() 

198 fitRes = fitter.fitWcs( 

199 matches=self.matches, 

200 initWcs=self.tanWcs, 

201 bbox=bbox, 

202 refCat=self.refCat, 

203 sourceCat=self.sourceCat, 

204 ) 

205 

206 self.checkResults(fitRes, catsUpdated=True) 

207 

208 

209class SideLoadTestCases: 

210 

211 """Base class implementations of testing methods. 

212 

213 Explicitly does not inherit from unittest.TestCase""" 

214 

215 def testTrivial(self): 

216 """Add no distortion""" 

217 self.doTest("testTrivial", lambda x, y: (x, y)) 

218 

219 def testOffset(self): 

220 """Add an offset""" 

221 self.doTest("testOffset", lambda x, y: (x + 5, y + 7)) 

222 

223 def testSkyOffset(self): 

224 """Add an on sky offset""" 

225 self.doTestAffine("testSkyOffset", 

226 [77, -200], 

227 np.array([[1.0, 0.0], [0.0, 1.0]])) 

228 self.doTestAffineReverse("testSkyOffsetRev", 

229 [77, -200], 

230 np.array([[1.0, 0.0], [0.0, 1.0]])) 

231 

232 def testAffine(self): 

233 """Add an Affine (shear + scale + rot) distortion""" 

234 self.doTestAffine("testAffine", 

235 [0, 0], 

236 np.array([[0.4, 0.1], [-0.21, 2.0]])) 

237 self.doTestAffineReverse("testAffineRev", 

238 [0, 0], 

239 np.array([[0.4, 0.1], [-0.21, 2.0]])) 

240 

241 def testAffineAndOffset(self): 

242 """Add a transform and offset""" 

243 self.doTestAffine("testAffineAndOffset", 

244 [30, 100], 

245 np.array([[0.5, 0.01], [-0.2, 0.3]])) 

246 self.doTestAffineReverse("testAffineAndOffsetRev", 

247 [30, 100], 

248 np.array([[0.5, 0.01], [-0.2, 0.3]])) 

249 

250 

251# The test classes inherit from two base classes and differ in the match 

252# class being used. 

253 

254class FitAffineWcsTaskTestCaseReferenceMatch(BaseTestCase, 

255 SideLoadTestCases, 

256 lsst.utils.tests.TestCase): 

257 MatchClass = afwTable.ReferenceMatch 

258 

259 

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

261 pass 

262 

263 

264def setup_module(module): 

265 lsst.utils.tests.init() 

266 

267 

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

269 lsst.utils.tests.init() 

270 unittest.main()